r_spec-clone 1.0.0.rc1 → 1.2.2

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: b96788e870f05feb6035cc289a06d274f8e68f1c6dbda8187db392d2ddf58cc5
4
- data.tar.gz: cecca9a87c7860b87aa2485846dadb23337a5292082e9d71685ed3bb61820968
3
+ metadata.gz: 7ca7e0d020bebba8aec767197a6c765fa4c0ded5bb96fb58723e5de8321ba1bf
4
+ data.tar.gz: 3e9fd96ec29c5341bb5b8f8a3fb60c9672a816e3dd22e1d80d7593ef78689f74
5
5
  SHA512:
6
- metadata.gz: aedab54edd51616dcb01f83ffe82e68285d3d3585c51cb3dc60c78c8d16230f3a5f78e9a886d678153cab84a6aa2e59b17d4d6b7cdb2301bb4ee3ba7687a8290
7
- data.tar.gz: f2654fab9bb2b84450c69e1ab94fca926b54551c6de307bb7432ece5aedabd8386b661926f65005f02fa4fbd33bd22c8173226ebe6f57001b747a8e1e74905de
6
+ metadata.gz: 7ae195dc7673cdd06068d376763ca3b67904eb1bea7ec502cdaf74424ffacb2d44c685988e7c22db21e7be2295a9d3de3b93d587e1c0d8696739dfdb8f37216d
7
+ data.tar.gz: 2b3522c31de373f4b9dc3a5d88a9a8e2ec7443d11205d6f93eae91f43a136bead79774333d3d8bac04b96cd07f89eb8c9792c379af0eb380b644da0931d639b4
data/README.md CHANGED
@@ -6,25 +6,25 @@ A minimalist __RSpec clone__ with all the essentials.
6
6
 
7
7
  ## Status
8
8
 
9
+ [![Home](https://img.shields.io/badge/Home-r--spec.dev-00af8b)](https://r-spec.dev/)
9
10
  [![Version](https://img.shields.io/github/v/tag/cyril/r_spec-clone.rb?label=Version&logo=github)](https://github.com/cyril/r_spec-clone.rb/releases)
10
- [![Yard documentation](https://img.shields.io/badge/Yard-documentation-blue.svg?)](https://rubydoc.info/github/cyril/r_spec-clone.rb/main)
11
+ [![Yard documentation](https://img.shields.io/badge/Yard-documentation-blue.svg?logo=github)](https://rubydoc.info/github/cyril/r_spec-clone.rb/main)
11
12
  [![CI](https://github.com/cyril/r_spec-clone.rb/workflows/CI/badge.svg?branch=main)](https://github.com/cyril/r_spec-clone.rb/actions?query=workflow%3Aci+branch%3Amain)
12
13
  [![RuboCop](https://github.com/cyril/r_spec-clone.rb/workflows/RuboCop/badge.svg?branch=main)](https://github.com/cyril/r_spec-clone.rb/actions?query=workflow%3Arubocop+branch%3Amain)
13
14
  [![License](https://img.shields.io/github/license/cyril/r_spec-clone.rb?label=License&logo=github)](https://github.com/cyril/r_spec-clone.rb/raw/main/LICENSE.md)
14
15
 
15
16
  ## Project goals
16
17
 
17
- 1. Keep a low level of code complexity and ensure thread safety.
18
- 2. The interface must translate into atomic and simple Ruby objects.
18
+ 1. Keep a low level of code complexity, avoid false negatives and false positives.
19
+ 2. Translate specification documents into atomic and thread safe Ruby objects.
19
20
  3. Avoid overloading the interface with additional alternative syntaxes.
20
21
  4. Provide most of RSpec's DSL to express expected outcomes of a code example.
21
22
 
22
23
  ## Some differences
23
24
 
24
- * Spec files can be executed with `ruby` directly.
25
25
  * There is no option to activate monkey-patching.
26
- * It does not rely on hacks such as [`at_exit` hook](https://blog.arkency.com/2013/06/are-we-abusing-at-exit/) to trigger the tests.
27
- * Built-in matchers [do not trust _actual_](https://asciinema.org/a/29172?autoplay=1&speed=2) and do not send it messages.
26
+ * It does not rely on [hacks such as `at_exit` hook](https://blog.arkency.com/2013/06/are-we-abusing-at-exit/) to trigger the tests.
27
+ * Malicious _actual values_ cannot [hack results](https://asciinema.org/a/423547?autoplay=1&speed=2).
28
28
  * If no `subject` has been explicitly determined, none is defined.
29
29
  * If no described class is set, `described_class` is undefined instead of `nil`.
30
30
  * Expectations cannot be added inside a `before` block.
@@ -32,14 +32,14 @@ A minimalist __RSpec clone__ with all the essentials.
32
32
  * The `let` method defines a helper method rather than a memoized helper method.
33
33
  * The one-liner `is_expected` syntax also works with block expectations.
34
34
  * `subject`, `before`, `after` and `let` definitions must come before examples.
35
- * Each [`context` runs its tests in _isolation_](https://asciinema.org/a/29070?autoplay=1&speed=2) to prevent side effects.
35
+ * Runs much faster.
36
36
 
37
37
  ## Installation
38
38
 
39
39
  Add this line to your application's Gemfile:
40
40
 
41
41
  ```ruby
42
- gem "r_spec-clone", ">= 1.0.0.rc1"
42
+ gem "r_spec-clone"
43
43
  ```
44
44
 
45
45
  And then execute:
@@ -51,7 +51,7 @@ bundle
51
51
  Or install it yourself as:
52
52
 
53
53
  ```sh
54
- gem install r_spec-clone --pre
54
+ gem install r_spec-clone
55
55
  ```
56
56
 
57
57
  ## Overview
@@ -68,7 +68,7 @@ A basic spec looks something like this:
68
68
 
69
69
  ### Anatomy of a spec file
70
70
 
71
- To use the `RSpec` module and its DSL, you need to add `require "r_spec/clone"` to your spec files.
71
+ To use the `RSpec` module and its DSL, you need to add `require "r_spec"` to your spec files.
72
72
  Many projects use a custom spec helper which organizes these includes.
73
73
 
74
74
  Concrete test cases are defined in `it` blocks.
@@ -79,6 +79,8 @@ Test cases that have been defined or outlined but are not yet expected to work c
79
79
  An `it` block contains an example that should invoke the code to be tested and define what is expected of it.
80
80
  Each example can contain multiple expectations, but it should test only one specific behaviour.
81
81
 
82
+ The `its` method can also be used to generate a nested example group with a single example that specifies the expected value (or the block expectations) of an attribute of the subject using `is_expected`.
83
+
82
84
  To express an expectation, wrap an object or block in `expect`, call `to` (or `not_to`) and pass it a matcher object.
83
85
  If the expectation is met, code execution continues.
84
86
  Otherwise the example has _failed_ and other code will not be executed.
@@ -93,8 +95,27 @@ For unit tests, it is recommended to follow the conventions for method names:
93
95
  * instance methods are prefixed with `#`, class methods with `.`.
94
96
 
95
97
  To establish certain contexts — think _empty array_ versus _array with elements_ — the `context` method may be used to communicate this to the reader.
96
- Its behavior is slightly different from `describe` because each `context` runs its tests in isolation,
97
- so side effects caused by testing do not propagate out of contexts.
98
+
99
+ To execute unit tests while isolating side effects in a sub-process, declined methods can be used: `describe!`, `context!`, `it!`, `its!`. Here is an example:
100
+
101
+ ```ruby
102
+ app = "foo"
103
+
104
+ RSpec.describe "Side effects per example" do
105
+ it! "runs the example in isolation" do
106
+ expect { app.gsub!("foo", "bar") }.to eq "bar"
107
+ expect(app).to eq "bar"
108
+ end
109
+
110
+ it "runs the example" do
111
+ expect(app).to eq "foo"
112
+ end
113
+ end
114
+
115
+ # Success: expected to eq "bar".
116
+ # Success: expected to eq "bar".
117
+ # Success: expected to eq "foo".
118
+ ```
98
119
 
99
120
  ### Expectations
100
121
 
@@ -211,11 +232,17 @@ bundle exec rake
211
232
 
212
233
  ## Performance
213
234
 
214
- ### Runtime
235
+ ### Boot time
236
+
237
+ Benchmark against [100 executions of a file containing 1 expectation](https://github.com/cyril/r_spec-clone.rb/blob/main/benchmark/boot_time/) (lower is better).
238
+
239
+ ![Boot time](https://r-spec.dev/benchmark-boot-time.svg)
240
+
241
+ ### Run time
215
242
 
216
- Benchmark against [100 executions of a file containing one expectation](https://github.com/cyril/r_spec-clone.rb/blob/main/benchmark/) (lower is better).
243
+ Benchmark against [1 execution of a file containing 1,000,000 expectations](https://github.com/cyril/r_spec-clone.rb/blob/main/benchmark/run_time/) (lower is better).
217
244
 
218
- ![Runtime](https://clone.r-spec.dev/benchmark-runtime.png)
245
+ ![Run time](https://r-spec.dev/benchmark-run-time.svg)
219
246
 
220
247
  ## Test suite
221
248
 
@@ -224,7 +251,8 @@ __RSpec clone__'s specifications are self-described here: [spec/](https://github
224
251
  ## Contact
225
252
 
226
253
  * Home page: [https://r-spec.dev/](https://r-spec.dev/)
227
- * Cheatsheet: [https://clone.r-spec.dev/cheatsheet.html](https://clone.r-spec.dev/cheatsheet.html)
254
+ * Cheatsheet: [https://r-spec.dev/cheatsheet.html](https://r-spec.dev/cheatsheet.html)
255
+ * Blog post: [https://batman.buzz/introducing-a-new-rspec-850d48c0f901](https://batman.buzz/introducing-a-new-rspec-850d48c0f901)
228
256
  * Source code: [https://github.com/cyril/r_spec-clone.rb](https://github.com/cyril/r_spec-clone.rb)
229
257
  * API Doc: [https://rubydoc.info/gems/r_spec-clone](https://rubydoc.info/gems/r_spec-clone)
230
258
  * Twitter: [https://twitter.com/cyri\_](https://twitter.com/cyri\_)
data/lib/r_spec.rb CHANGED
@@ -5,7 +5,7 @@ require_relative File.join("r_spec", "clone", "dsl")
5
5
  # Top level namespace for the RSpec clone.
6
6
  #
7
7
  # @example The true from the false
8
- # require "r_spec/clone"
8
+ # require "r_spec"
9
9
  #
10
10
  # RSpec.describe "The true from the false" do
11
11
  # it { expect(false).not_to be true }
@@ -15,7 +15,7 @@ require_relative File.join("r_spec", "clone", "dsl")
15
15
  # # Success: expected false not to be true.
16
16
  #
17
17
  # @example The basic behavior of arrays
18
- # require "r_spec/clone"
18
+ # require "r_spec"
19
19
  #
20
20
  # RSpec.describe Array do
21
21
  # describe "#size" do
@@ -41,7 +41,7 @@ require_relative File.join("r_spec", "clone", "dsl")
41
41
  # # Success: expected false to be false.
42
42
  #
43
43
  # @example An inherited definition of let
44
- # require "r_spec/clone"
44
+ # require "r_spec"
45
45
  #
46
46
  # RSpec.describe Integer do
47
47
  # let(:answer) { 42 }
@@ -68,11 +68,8 @@ module RSpec
68
68
  # Defines an example group that establishes a specific context, like _empty
69
69
  # array_ versus _array with elements_.
70
70
  #
71
- # Unlike {.describe}, the block is evaluated in isolation in order to scope
72
- # possible side effects inside its context.
73
- #
74
71
  # @example
75
- # require "r_spec/clone"
72
+ # require "r_spec"
76
73
  #
77
74
  # RSpec.context "when divided by zero" do
78
75
  # subject { 42 / 0 }
@@ -86,20 +83,62 @@ module RSpec
86
83
  # @param description [String] A description that usually begins with "when",
87
84
  # "with" or "without".
88
85
  # @param block [Proc] The block to define the specs.
89
- #
90
- # @api public
91
86
  def self.context(description, &block)
92
87
  Clone::Dsl.context(description, &block)
93
88
  end
94
89
 
90
+ # :nocov:
91
+
92
+ # Runs a context example group in a subprocess to isolate side effects.
93
+ #
94
+ # @example
95
+ # str = "Hello, world!"
96
+ #
97
+ # require "r_spec"
98
+ #
99
+ # RSpec.context! "when a string becomes uppercase" do
100
+ # before do
101
+ # str.upcase!
102
+ # end
103
+ #
104
+ # it { expect(str).to eq "HELLO, WORLD!" }
105
+ # end
106
+ #
107
+ # # Output to the console
108
+ # # Success: expected to eq "HELLO, WORLD!".
109
+ #
110
+ # RSpec.it { expect(str).to eq "Hello, world!" }
111
+ #
112
+ # # Output to the console
113
+ # # Success: expected to eq "Hello, world!".
114
+ #
115
+ # @param (see #context)
116
+ def self.context!(description, &block)
117
+ Clone::Dsl.context!(description, &block)
118
+ end
119
+
120
+ # :nocov:
121
+
95
122
  # Defines an example group that describes a unit to be tested.
96
123
  #
97
124
  # @example
98
- # require "r_spec/clone"
125
+ # require "r_spec"
99
126
  #
100
127
  # RSpec.describe String do
128
+ # it { expect(described_class).to be String }
129
+ # end
130
+ #
131
+ # # Output to the console
132
+ # # Success: expected to be String.
133
+ #
134
+ # @example
135
+ # require "r_spec"
136
+ #
137
+ # RSpec.describe String do
138
+ # let(:foo) { "foo" }
139
+ #
101
140
  # describe "+" do
102
- # it("concats") { expect("foo" + "bar").to eq "foobar" }
141
+ # it("concats") { expect(foo + "bar").to eq "foobar" }
103
142
  # end
104
143
  # end
105
144
  #
@@ -108,18 +147,48 @@ module RSpec
108
147
  #
109
148
  # @param const [Module, String] A module to include in block context.
110
149
  # @param block [Proc] The block to define the specs.
111
- #
112
- # @api public
113
150
  def self.describe(const, &block)
114
151
  Clone::Dsl.describe(const, &block)
115
152
  end
116
153
 
154
+ # :nocov:
155
+
156
+ # Runs a describe example group in a subprocess to isolate side effects.
157
+ #
158
+ # @example
159
+ # $app = "foo"
160
+ #
161
+ # require "r_spec"
162
+ #
163
+ # RSpec.describe! "#gsub!" do
164
+ # before do
165
+ # $app.gsub!("o", "0")
166
+ # end
167
+ #
168
+ # it { expect($app).to eq "f00" }
169
+ # end
170
+ #
171
+ # # Output to the console
172
+ # # Success: expected to eq "f00".
173
+ #
174
+ # RSpec.it { expect($app).to eq "foo" }
175
+ #
176
+ # # Output to the console
177
+ # # Success: expected to eq "foo".
178
+ #
179
+ # @param (see #describe)
180
+ def self.describe!(const, &block)
181
+ Clone::Dsl.describe!(const, &block)
182
+ end
183
+
184
+ # :nocov:
185
+
117
186
  # Defines a concrete test case.
118
187
  #
119
188
  # The test is performed by the block supplied to &block.
120
189
  #
121
190
  # @example The integer after 41
122
- # require "r_spec/clone"
191
+ # require "r_spec"
123
192
  #
124
193
  # RSpec.it { expect(41.next).to be 42 }
125
194
  #
@@ -132,21 +201,48 @@ module RSpec
132
201
  # @param name [String, nil] The name of the spec.
133
202
  # @param block [Proc] An expectation to evaluate.
134
203
  #
135
- # @raise (see RSpec::ExpectationTarget::Base#result)
136
- # @return (see RSpec::ExpectationTarget::Base#result)
137
- #
138
- # @api public
204
+ # @raise (see RSpec::Clone::ExpectationTarget::Base#result)
205
+ # @return (see RSpec::Clone::ExpectationTarget::Base#result)
139
206
  def self.it(name = nil, &block)
140
207
  Clone::Dsl.it(name, &block)
141
208
  end
142
209
 
210
+ # :nocov:
211
+
212
+ # Runs a concrete test case in a subprocess to isolate side effects.
213
+ #
214
+ # @example
215
+ # app = "Hello, world!"
216
+ #
217
+ # require "r_spec"
218
+ #
219
+ # RSpec.it! { expect(app.gsub!("world", "Alice")).to eq "Hello, Alice!" }
220
+ #
221
+ # # Output to the console
222
+ # # Success: expected to eq "Hello, Alice!".
223
+ #
224
+ # RSpec.it { expect(app).to eq "Hello, world!" }
225
+ #
226
+ # # Output to the console
227
+ # # Success: expected to eq "Hello, world!".
228
+ #
229
+ # @param (see #it)
230
+ #
231
+ # @raise (see ExpectationTarget::Base#result)
232
+ # @return (see ExpectationTarget::Base#result)
233
+ def self.it!(name = nil, &block)
234
+ Clone::Dsl.it!(name, &block)
235
+ end
236
+
237
+ # :nocov:
238
+
143
239
  # Defines a pending test case.
144
240
  #
145
241
  # `&block` is never evaluated. It can be used to describe behaviour that is
146
242
  # not yet implemented.
147
243
  #
148
244
  # @example
149
- # require "r_spec/clone"
245
+ # require "r_spec"
150
246
  #
151
247
  # RSpec.pending "is implemented but waiting" do
152
248
  # expect something to be finished
@@ -164,8 +260,6 @@ module RSpec
164
260
  # @param message [String] The reason why the example is pending.
165
261
  #
166
262
  # @return [nil] Write a message to STDOUT.
167
- #
168
- # @api public
169
263
  def self.pending(message)
170
264
  Clone::Dsl.pending(message)
171
265
  end
@@ -3,8 +3,6 @@
3
3
  module RSpec
4
4
  module Clone
5
5
  # Send log messages to the console.
6
- #
7
- # @api private
8
6
  module Console
9
7
  # @param report [::Expresenter::Pass] Passed expectation result presenter.
10
8
  #
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "aw"
4
-
5
3
  require_relative "console"
6
4
  require_relative "error"
7
5
  require_relative "expectation_helper"
@@ -18,7 +16,7 @@ module RSpec
18
16
  # Executes the given block before each spec in the current context runs.
19
17
  #
20
18
  # @example
21
- # require "r_spec/clone"
19
+ # require "r_spec"
22
20
  #
23
21
  # RSpec.describe Integer do
24
22
  # before do
@@ -44,6 +42,8 @@ module RSpec
44
42
  # # Success: expected to be 123.
45
43
  #
46
44
  # @param block [Proc] The content to execute at the class initialization.
45
+ #
46
+ # @api public
47
47
  def self.before(&block)
48
48
  define_method(BEFORE_METHOD) do
49
49
  super()
@@ -56,7 +56,7 @@ module RSpec
56
56
  # Executes the given block after each spec in the current context runs.
57
57
  #
58
58
  # @example
59
- # require "r_spec/clone"
59
+ # require "r_spec"
60
60
  #
61
61
  # RSpec.describe Integer do
62
62
  # after do
@@ -71,6 +71,8 @@ module RSpec
71
71
  # # That is the answer to everything.
72
72
  #
73
73
  # @param block [Proc] The content to execute at the class initialization.
74
+ #
75
+ # @api public
74
76
  def self.after(&block)
75
77
  define_method(AFTER_METHOD) do
76
78
  instance_exec(&block)
@@ -83,7 +85,7 @@ module RSpec
83
85
  # Sets a user-defined property.
84
86
  #
85
87
  # @example
86
- # require "r_spec/clone"
88
+ # require "r_spec"
87
89
  #
88
90
  # RSpec.describe "Name stories" do
89
91
  # let(:name) { "Bob" }
@@ -105,6 +107,8 @@ module RSpec
105
107
  # @param block [Proc] The content of the method to define.
106
108
  #
107
109
  # @return [Symbol] A private method that define the block content.
110
+ #
111
+ # @api public
108
112
  def self.let(name, *args, **kwargs, &block)
109
113
  raise Error::ReservedMethod if [BEFORE_METHOD, AFTER_METHOD].include?(name.to_sym)
110
114
 
@@ -114,7 +118,7 @@ module RSpec
114
118
  # Sets a user-defined property named {#subject}.
115
119
  #
116
120
  # @example
117
- # require "r_spec/clone"
121
+ # require "r_spec"
118
122
  #
119
123
  # RSpec.describe Array do
120
124
  # subject { [1, 2, 3] }
@@ -122,13 +126,23 @@ module RSpec
122
126
  # it "has the prescribed elements" do
123
127
  # expect(subject).to eq([1, 2, 3])
124
128
  # end
129
+ #
130
+ # it { is_expected.to be_an_instance_of described_class }
131
+ #
132
+ # its(:size) { is_expected.to be 3 }
133
+ # its(:downcase) { is_expected.to raise_exception NoMethodError }
125
134
  # end
126
135
  #
127
136
  # # Output to the console
128
137
  # # Success: expected to eq [1, 2, 3].
138
+ # # Success: expected [1, 2, 3] to be an instance of Array.
139
+ # # Success: expected to be 3.
140
+ # # Success: undefined method `downcase' for [1, 2, 3]:Array.
129
141
  #
130
142
  # @param block [Proc] The subject to set.
131
143
  # @return [Symbol] A {#subject} method that define the block content.
144
+ #
145
+ # @api public
132
146
  def self.subject(&block)
133
147
  let(__method__, &block)
134
148
  end
@@ -136,7 +150,7 @@ module RSpec
136
150
  # Defines an example group that describes a unit to be tested.
137
151
  #
138
152
  # @example
139
- # require "r_spec/clone"
153
+ # require "r_spec"
140
154
  #
141
155
  # RSpec.describe String do
142
156
  # describe "+" do
@@ -149,20 +163,62 @@ module RSpec
149
163
  #
150
164
  # @param const [Module, String] A module to include in block context.
151
165
  # @param block [Proc] The block to define the specs.
166
+ #
167
+ # @api public
152
168
  def self.describe(const, &block)
153
169
  desc = ::Class.new(self)
154
170
  desc.let(:described_class) { const } if const.is_a?(::Module)
155
171
  desc.instance_eval(&block)
156
172
  end
157
173
 
158
- # Defines an example group that establishes a specific context, like _empty
159
- # array_ versus _array with elements_.
174
+ # :nocov:
175
+
176
+ # Runs a describe example group in a subprocess to isolate side effects.
177
+ #
178
+ # @example
179
+ # $app = "foo"
180
+ #
181
+ # require "r_spec"
160
182
  #
161
- # Unlike {.describe}, the block is evaluated in isolation in order to scope
162
- # possible side effects inside its context.
183
+ # RSpec.describe "Scoped side effects" do
184
+ # describe! "#gsub!" do
185
+ # before do
186
+ # $app.gsub!("o", "0")
187
+ # end
188
+ #
189
+ # context! "when isolated in the context" do
190
+ # before do
191
+ # $app.gsub!("f", "F")
192
+ # end
193
+ #
194
+ # it { expect($app).to eq "F00" }
195
+ # end
196
+ #
197
+ # it { expect($app).to eq "f00" }
198
+ # end
199
+ #
200
+ # it { expect($app).to eq "foo" }
201
+ # end
202
+ #
203
+ # # Output to the console
204
+ # # Success: expected to eq "F00".
205
+ # # Success: expected to eq "f00".
206
+ # # Success: expected to eq "foo".
207
+ #
208
+ # @param (see #describe)
209
+ #
210
+ # @api public
211
+ def self.describe!(const, &block)
212
+ fork! { describe(const, &block) }
213
+ end
214
+
215
+ # :nocov:
216
+
217
+ # Defines an example group that establishes a specific context, like
218
+ # _empty array_ versus _array with elements_.
163
219
  #
164
220
  # @example
165
- # require "r_spec/clone"
221
+ # require "r_spec"
166
222
  #
167
223
  # RSpec.describe "web resource" do
168
224
  # context "when resource is not found" do
@@ -181,17 +237,63 @@ module RSpec
181
237
  # @param _description [String] A description that usually begins with
182
238
  # "when", "with" or "without".
183
239
  # @param block [Proc] The block to define the specs.
240
+ #
241
+ # @api public
184
242
  def self.context(_description, &block)
185
243
  desc = ::Class.new(self)
186
- ::Aw.fork! { desc.instance_eval(&block) }
244
+ desc.instance_eval(&block)
245
+ end
246
+
247
+ # :nocov:
248
+
249
+ # Runs a context example group in a subprocess to isolate side effects.
250
+ #
251
+ # @example
252
+ # app = "Hello, world!"
253
+ #
254
+ # require "r_spec"
255
+ #
256
+ # RSpec.describe String do
257
+ # subject do
258
+ # app
259
+ # end
260
+ #
261
+ # before do
262
+ # subject.gsub!("world", person)
263
+ # end
264
+ #
265
+ # context! "when Alice is greeted" do
266
+ # let(:person) { "Alice" }
267
+ #
268
+ # it { is_expected.to eq "Hello, Alice!" }
269
+ # end
270
+ #
271
+ # context! "when Bob is greeted" do
272
+ # let(:person) { "Bob" }
273
+ #
274
+ # it { is_expected.to eq "Hello, Bob!" }
275
+ # end
276
+ # end
277
+ #
278
+ # # Output to the console
279
+ # # Success: expected to eq "Hello, Alice!".
280
+ # # Success: expected to eq "Hello, Bob!".
281
+ #
282
+ # @param (see #context)
283
+ #
284
+ # @api public
285
+ def self.context!(description, &block)
286
+ fork! { context(description, &block) }
187
287
  end
188
288
 
289
+ # :nocov:
290
+
189
291
  # Defines a concrete test case.
190
292
  #
191
293
  # The test is performed by the block supplied to `&block`.
192
294
  #
193
295
  # @example The integer after 41
194
- # require "r_spec/clone"
296
+ # require "r_spec"
195
297
  #
196
298
  # RSpec.describe Integer do
197
299
  # it { expect(41.next).to be 42 }
@@ -201,7 +303,7 @@ module RSpec
201
303
  # # Success: expected to be 42.
202
304
  #
203
305
  # @example A division by zero
204
- # require "r_spec/clone"
306
+ # require "r_spec"
205
307
  #
206
308
  # RSpec.describe Integer do
207
309
  # subject { 41 }
@@ -224,25 +326,54 @@ module RSpec
224
326
  #
225
327
  # @raise (see ExpectationTarget::Base#result)
226
328
  # @return (see ExpectationTarget::Base#result)
329
+ #
330
+ # @api public
227
331
  def self.it(_name = nil, &block)
228
- raise ::ArgumentError, "Missing example block" unless block
332
+ run(example_without_attribute.new, &block)
333
+ end
229
334
 
230
- example = ::Class.new(self) { include ExpectationHelper::It }.new
231
- example.instance_eval(&block)
232
- rescue ::SystemExit
233
- Console.source(*block.source_location)
335
+ # :nocov:
234
336
 
235
- exit false
236
- ensure
237
- example&.send(AFTER_METHOD)
337
+ # Runs a concrete test case in a subprocess to isolate side effects.
338
+ #
339
+ # @example
340
+ # app = "foo"
341
+ #
342
+ # require "r_spec"
343
+ #
344
+ # RSpec.describe "Side effects per example" do
345
+ # it! "runs the example in isolation" do
346
+ # expect { app.gsub!("foo", "bar") }.to eq "bar"
347
+ # expect(app).to eq "bar"
348
+ # end
349
+ #
350
+ # it "runs the example" do
351
+ # expect(app).to eq "foo"
352
+ # end
353
+ # end
354
+ #
355
+ # # Output to the console
356
+ # # Success: expected to eq "bar".
357
+ # # Success: expected to eq "bar".
358
+ # # Success: expected to eq "foo".
359
+ #
360
+ # @param (see #it)
361
+ #
362
+ # @raise (see ExpectationTarget::Base#result)
363
+ # @return (see ExpectationTarget::Base#result)
364
+ #
365
+ # @api public
366
+ def self.it!(name = nil, &block)
367
+ fork! { it(name, &block) }
238
368
  end
239
369
 
240
- # Use the {.its} method to define a single spec that specifies the actual
241
- # value of an attribute of the subject using
242
- # {ExpectationHelper::Its#is_expected}.
370
+ # :nocov:
371
+
372
+ # Defines a single concrete test case that specifies the actual value of
373
+ # an attribute of the subject using {ExpectationHelper::Its#is_expected}.
243
374
  #
244
375
  # @example The integer after 41
245
- # require "r_spec/clone"
376
+ # require "r_spec"
246
377
  #
247
378
  # RSpec.describe Integer do
248
379
  # subject { 41 }
@@ -254,7 +385,7 @@ module RSpec
254
385
  # # Success: expected to be 42.
255
386
  #
256
387
  # @example A division by zero
257
- # require "r_spec/clone"
388
+ # require "r_spec"
258
389
  #
259
390
  # RSpec.describe Integer do
260
391
  # subject { 41 }
@@ -266,10 +397,10 @@ module RSpec
266
397
  # # Success: divided by 0.
267
398
  #
268
399
  # @example A spec without subject
269
- # require "r_spec/clone"
400
+ # require "r_spec"
270
401
  #
271
402
  # RSpec.describe Integer do
272
- # its(:boom) { is_expected.to raise_exception RSpec::Error::UndefinedSubject }
403
+ # its(:abs) { is_expected.to raise_exception RSpec::Clone::Error::UndefinedSubject }
273
404
  # end
274
405
  #
275
406
  # # Output to the console
@@ -282,33 +413,57 @@ module RSpec
282
413
  #
283
414
  # @raise (see ExpectationTarget::Base#result)
284
415
  # @return (see ExpectationTarget::Base#result)
416
+ #
417
+ # @api public
285
418
  def self.its(attribute, *args, **kwargs, &block)
286
- raise ::ArgumentError, "Missing example block" unless block
287
-
288
- example = ::Class.new(self) do
289
- include ExpectationHelper::Its
290
-
291
- define_method(:actual) do
292
- subject.public_send(attribute, *args, **kwargs)
293
- end
294
- end.new
419
+ run(example_with_attribute(attribute, *args, **kwargs).new, &block)
420
+ end
295
421
 
296
- example.instance_eval(&block)
297
- rescue ::SystemExit
298
- Console.source(*block.source_location)
422
+ # :nocov:
299
423
 
300
- exit false
301
- ensure
302
- example&.send(AFTER_METHOD)
424
+ # Runs a single concrete test case in a subprocess to isolate side
425
+ # effects.
426
+ #
427
+ # @example
428
+ # app = "foo"
429
+ #
430
+ # require "r_spec"
431
+ #
432
+ # RSpec.describe "Isolated side effect" do
433
+ # subject do
434
+ # app
435
+ # end
436
+ #
437
+ # its!(:upcase) { is_expected.to eq "FOO" }
438
+ #
439
+ # it "tests the original value" do
440
+ # expect(app).to eq "foo"
441
+ # end
442
+ # end
443
+ #
444
+ # # Output to the console
445
+ # # Success: expected to eq "FOO".
446
+ # # Success: expected to eq "foo".
447
+ #
448
+ # @param (see #it)
449
+ #
450
+ # @raise (see ExpectationTarget::Base#result)
451
+ # @return (see ExpectationTarget::Base#result)
452
+ #
453
+ # @api public
454
+ def self.its!(attribute, *args, **kwargs, &block)
455
+ fork! { its(attribute, *args, **kwargs, &block) }
303
456
  end
304
457
 
458
+ # :nocov:
459
+
305
460
  # Defines a pending test case.
306
461
  #
307
- # `&block` is never evaluated. It can be used to describe behaviour that is
308
- # not yet implemented.
462
+ # `&block` is never evaluated. It can be used to describe behaviour that
463
+ # is not yet implemented.
309
464
  #
310
465
  # @example
311
- # require "r_spec/clone"
466
+ # require "r_spec"
312
467
  #
313
468
  # RSpec.describe "an example" do
314
469
  # pending "is implemented but waiting" do
@@ -331,13 +486,56 @@ module RSpec
331
486
  Console.passed_spec Error::PendingExpectation.result(message)
332
487
  end
333
488
 
489
+ # Example class for concrete test case.
490
+ def self.example_without_attribute
491
+ ::Class.new(self) do
492
+ prepend ExpectationHelper::It
493
+ end
494
+ end
495
+
496
+ # Example class for concrete test case that specifies the actual value of
497
+ # an attribute of the subject.
498
+ def self.example_with_attribute(attribute, *args, **kwargs)
499
+ ::Class.new(self) do
500
+ prepend ExpectationHelper::Its
501
+
502
+ define_method(:actual) do
503
+ subject.public_send(attribute, *args, **kwargs)
504
+ end
505
+ end
506
+ end
507
+
508
+ # Creates a subprocess and runs the block inside.
509
+ def self.fork!(&block)
510
+ pid = fork(&block)
511
+ thread = ::Process.detach(pid)
512
+ exitstatus = thread.join.value.exitstatus
513
+ exit false unless exitstatus.zero?
514
+ end
515
+
516
+ # Execution of specifications.
517
+ def self.run(example, &block)
518
+ example.instance_eval(&block)
519
+ rescue ::SystemExit
520
+ Console.source(*block.source_location)
521
+
522
+ exit false
523
+ ensure
524
+ example&.send(AFTER_METHOD)
525
+ end
526
+
527
+ private_class_method :example_without_attribute, :example_with_attribute, :fork!, :run
528
+
334
529
  private
335
530
 
531
+ # If the first argument to a {.describe} definition is a class (or a
532
+ # module), this method will be overridden to return it.
336
533
  def described_class
337
534
  raise Error::UndefinedDescribedClass,
338
535
  "the first argument to at least one example group must be a module"
339
536
  end
340
537
 
538
+ # If a subject is defined, this method will be overridden to return it.
341
539
  def subject
342
540
  raise Error::UndefinedSubject, "subject not explicitly defined"
343
541
  end
@@ -8,8 +8,6 @@ require_relative File.join("error", "undefined_subject")
8
8
  module RSpec
9
9
  module Clone
10
10
  # Namespace for exceptions.
11
- #
12
- # @api private
13
11
  module Error
14
12
  end
15
13
  end
@@ -6,8 +6,6 @@ module RSpec
6
6
  module Clone
7
7
  module Error
8
8
  # Exception for pending expectations.
9
- #
10
- # @api private
11
9
  class PendingExpectation < ::RuntimeError
12
10
  # @param message [String] The not implemented expectation description.
13
11
  #
@@ -4,8 +4,6 @@ module RSpec
4
4
  module Clone
5
5
  module Error
6
6
  # Exception for reserved methods.
7
- #
8
- # @api private
9
7
  class ReservedMethod < ::RuntimeError
10
8
  end
11
9
  end
@@ -4,8 +4,6 @@ module RSpec
4
4
  module Clone
5
5
  module Error
6
6
  # Exception for undefined described classes.
7
- #
8
- # @api private
9
7
  class UndefinedDescribedClass < ::RuntimeError
10
8
  end
11
9
  end
@@ -4,8 +4,6 @@ module RSpec
4
4
  module Clone
5
5
  module Error
6
6
  # Exception for undefined subjects.
7
- #
8
- # @api private
9
7
  class UndefinedSubject < ::RuntimeError
10
8
  end
11
9
  end
@@ -18,8 +18,8 @@ module RSpec
18
18
  # @return [Block, Value] The wrapped target of an expectation.
19
19
  #
20
20
  # @example
21
- # expect("foo") # => #<RSpec::ExpectationTarget::Value:0x00007fb6b823 @actual="foo">
22
- # expect { Boom } # => #<RSpec::ExpectationTarget::Block:0x00007fb6b826 @callable=#<Proc:0x00007fb6b826>>
21
+ # expect("foo") # => #<RSpec::Clone::ExpectationTarget::Value:0x00007f @actual="foo">
22
+ # expect { Boom } # => #<RSpec::Clone::ExpectationTarget::Block:0x00007f @callable=#<Proc:0x00007f>>
23
23
  #
24
24
  # @api public
25
25
  def expect(value = self.class.superclass, &block)
@@ -31,7 +31,7 @@ module RSpec
31
31
  # @return [Block] The wrapped target of an expectation.
32
32
  #
33
33
  # @example
34
- # is_expected # => #<RSpec::ExpectationTarget::Block:0x00007fb6b8263df8 @callable=#<Proc:0x00007fb6b8263e20>>
34
+ # is_expected # => #<RSpec::Clone::ExpectationTarget::Block:0x00007fb6b8 @callable=#<Proc:0x00007fb6b8>>
35
35
  #
36
36
  # @api public
37
37
  def is_expected
@@ -15,7 +15,7 @@ module RSpec
15
15
  # @return [Block] The wrapped target of an expectation.
16
16
  #
17
17
  # @example
18
- # is_expected # => #<RSpec::ExpectationTarget::Block:0x00007fb6b8263df8 @callable=#<Proc:0x00007fb6b8263e20>>
18
+ # is_expected # => #<RSpec::Clone::ExpectationTarget::Block:0x00007f @callable=#<Proc:0x00007f>>
19
19
  #
20
20
  # @api public
21
21
  def is_expected
@@ -2,15 +2,13 @@
2
2
 
3
3
  require "matchi/rspec"
4
4
 
5
- require_relative File.join("..", "error", "pending_expectation")
6
-
7
5
  module RSpec
8
6
  module Clone
9
7
  module ExpectationHelper
10
8
  # Abstract expectation helper base module.
11
9
  #
12
- # This module defines a number of methods to create expectations, which are
13
- # automatically included into examples.
10
+ # This module defines a number of methods to create expectations, which
11
+ # are automatically included into examples.
14
12
  #
15
13
  # It also includes a collection of expectation matchers 🤹
16
14
  #
@@ -6,12 +6,10 @@ require_relative File.join("expectation_target", "value")
6
6
  module RSpec
7
7
  module Clone
8
8
  # Wraps the target of an expectation.
9
- #
10
- # @api private
11
9
  module ExpectationTarget
12
10
  # @param undefined_value A sentinel value to be able to tell when the user
13
- # did not pass an argument. We can't use `nil` for that because `nil` is a
14
- # valid value to pass.
11
+ # did not pass an argument. We can't use `nil` for that because `nil` is
12
+ # a valid value to pass.
15
13
  # @param value [#object_id, nil] An actual value.
16
14
  # @param block [#call, nil] A code to evaluate.
17
15
  #
@@ -10,14 +10,14 @@ module RSpec
10
10
  module ExpectationTarget
11
11
  # Abstract expectation target base class.
12
12
  #
13
- # @note `RSpec::ExpectationTarget::Base` is not intended to be instantiated
14
- # directly by users. Use `expect` instead.
13
+ # @note `RSpec::Clone::ExpectationTarget::Base` is not intended to be
14
+ # instantiated directly by users. Use `expect` instead.
15
15
  class Base
16
16
  # Instantiate a new expectation target.
17
17
  #
18
- # @param actual [#object_id] The actual value of the code to evaluate.
19
- def initialize(actual)
20
- @actual = actual
18
+ # @param input [#object_id, Proc] The code to evaluate.
19
+ def initialize(input)
20
+ @input = input
21
21
  end
22
22
 
23
23
  # Runs the given expectation, passing if `matcher` returns true.
@@ -52,6 +52,36 @@ module RSpec
52
52
 
53
53
  protected
54
54
 
55
+ # @param test [::TestTube::Base] The state of the experiment.
56
+ # @param matcher [#matches?] The matcher.
57
+ # @param negate [Boolean] The assertion is positive or negative.
58
+ #
59
+ # @return [nil] Write a message to STDOUT.
60
+ #
61
+ # @raise [SystemExit] Terminate execution immediately by calling
62
+ # `Kernel.exit(false)` with a failure message written to STDERR.
63
+ def absolute_requirement(test, matcher:, negate:)
64
+ result(
65
+ passed?(test),
66
+ actual: test.actual,
67
+ error: test.error,
68
+ got: test.got,
69
+ matcher: matcher,
70
+ negate: negate
71
+ )
72
+ end
73
+
74
+ # Code experiment result.
75
+ #
76
+ # @param test [::TestTube::Base] The state of the experiment.
77
+ #
78
+ # @see https://github.com/fixrb/test_tube
79
+ #
80
+ # @return [Boolean] The result of the test (passed or failed).
81
+ def passed?(test)
82
+ test.got.equal?(true)
83
+ end
84
+
55
85
  # @param passed [Boolean] The high expectation passed or failed.
56
86
  # @param actual [#object_id] The actual value.
57
87
  # @param error [Exception, nil] Any raised exception.
@@ -63,8 +93,6 @@ module RSpec
63
93
  #
64
94
  # @raise [SystemExit] Terminate execution immediately by calling
65
95
  # `Kernel.exit(false)` with a failure message written to STDERR.
66
- #
67
- # @api private
68
96
  def result(passed, actual:, error:, got:, matcher:, negate:)
69
97
  Console.passed_spec ::Expresenter.call(passed).with(
70
98
  actual: actual,
@@ -16,31 +16,20 @@ module RSpec
16
16
  # # with `not_to`
17
17
  # expect { actual }.not_to be(4)
18
18
  #
19
- # @note `RSpec::ExpectationTarget::Block` is not intended to be instantiated
20
- # directly by users. Use `expect` instead.
19
+ # @note `RSpec::Clone::ExpectationTarget::Block` is not intended to be
20
+ # instantiated directly by users. Use `expect` instead.
21
21
  class Block < Base
22
22
  protected
23
23
 
24
24
  # @param matcher [#matches?] The matcher.
25
25
  # @param negate [Boolean] The assertion is positive or negative.
26
26
  #
27
- # @return [nil] Write a message to STDOUT.
27
+ # @return (see Base#absolute_requirement)
28
28
  #
29
- # @raise [SystemExit] Terminate execution immediately by calling
30
- # `Kernel.exit(false)` with a failure message written to STDERR.
29
+ # @raise (see Base#absolute_requirement)
31
30
  def absolute_requirement(matcher:, negate:)
32
- experiment = ::TestTube.invoke(
33
- @actual,
34
- isolation: false,
35
- matcher: matcher,
36
- negate: negate
37
- )
38
-
39
- result(
40
- experiment.got.equal?(true),
41
- actual: experiment.actual,
42
- error: experiment.error,
43
- got: experiment.got,
31
+ super(
32
+ ::TestTube.invoke(isolation: false, matcher: matcher, negate: negate, &@input),
44
33
  matcher: matcher,
45
34
  negate: negate
46
35
  )
@@ -16,30 +16,20 @@ module RSpec
16
16
  # # with `not_to`
17
17
  # expect(actual).not_to be(4)
18
18
  #
19
- # @note `RSpec::ExpectationTarget::Value` is not intended to be instantiated
20
- # directly by users. Use `expect` instead.
19
+ # @note `RSpec::Clone::ExpectationTarget::Value` is not intended to be
20
+ # instantiated directly by users. Use `expect` instead.
21
21
  class Value < Base
22
22
  protected
23
23
 
24
24
  # @param matcher [#matches?] The matcher.
25
25
  # @param negate [Boolean] The assertion is positive or negative.
26
26
  #
27
- # @return [nil] Write a message to STDOUT.
27
+ # @return (see Base#absolute_requirement)
28
28
  #
29
- # @raise [SystemExit] Terminate execution immediately by calling
30
- # `Kernel.exit(false)` with a failure message written to STDERR.
29
+ # @raise (see Base#absolute_requirement)
31
30
  def absolute_requirement(matcher:, negate:)
32
- experiment = ::TestTube.pass(
33
- @actual,
34
- matcher: matcher,
35
- negate: negate
36
- )
37
-
38
- result(
39
- experiment.got.equal?(true),
40
- actual: experiment.actual,
41
- error: experiment.error,
42
- got: experiment.got,
31
+ super(
32
+ ::TestTube.pass(@input, matcher: matcher, negate: negate),
43
33
  matcher: matcher,
44
34
  negate: negate
45
35
  )
metadata CHANGED
@@ -1,29 +1,15 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: r_spec-clone
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0.rc1
4
+ version: 1.2.2
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-27 00:00:00.000000000 Z
11
+ date: 2021-07-12 00:00:00.000000000 Z
12
12
  dependencies:
13
- - !ruby/object:Gem::Dependency
14
- name: aw
15
- requirement: !ruby/object:Gem::Requirement
16
- requirements:
17
- - - "~>"
18
- - !ruby/object:Gem::Version
19
- version: 0.1.12
20
- type: :runtime
21
- prerelease: false
22
- version_requirements: !ruby/object:Gem::Requirement
23
- requirements:
24
- - - "~>"
25
- - !ruby/object:Gem::Version
26
- version: 0.1.12
27
13
  - !ruby/object:Gem::Dependency
28
14
  name: expresenter
29
15
  requirement: !ruby/object:Gem::Requirement
@@ -58,14 +44,14 @@ dependencies:
58
44
  requirements:
59
45
  - - "~>"
60
46
  - !ruby/object:Gem::Version
61
- version: 1.1.0
47
+ version: 2.0.0
62
48
  type: :runtime
63
49
  prerelease: false
64
50
  version_requirements: !ruby/object:Gem::Requirement
65
51
  requirements:
66
52
  - - "~>"
67
53
  - !ruby/object:Gem::Version
68
- version: 1.1.0
54
+ version: 2.0.0
69
55
  - !ruby/object:Gem::Dependency
70
56
  name: bundler
71
57
  requirement: !ruby/object:Gem::Requirement
@@ -217,7 +203,7 @@ files:
217
203
  - lib/r_spec/clone/expectation_target/base.rb
218
204
  - lib/r_spec/clone/expectation_target/block.rb
219
205
  - lib/r_spec/clone/expectation_target/value.rb
220
- homepage: https://clone.r-spec.dev/
206
+ homepage: https://r-spec.dev/
221
207
  licenses:
222
208
  - MIT
223
209
  metadata:
@@ -236,9 +222,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
236
222
  version: 2.7.0
237
223
  required_rubygems_version: !ruby/object:Gem::Requirement
238
224
  requirements:
239
- - - ">"
225
+ - - ">="
240
226
  - !ruby/object:Gem::Version
241
- version: 1.3.1
227
+ version: '0'
242
228
  requirements: []
243
229
  rubygems_version: 3.1.6
244
230
  signing_key: