r_spec-clone 1.7.2 → 2.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.
@@ -1,91 +1,123 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "logger"
3
+ require "defi"
4
+
4
5
  require_relative "error"
5
6
  require_relative "expectation_helper"
7
+ require_relative "report"
6
8
 
7
9
  module RSpec
8
10
  module Clone
9
- # Abstract class for handling the domain-specific language.
11
+ # The language of a spec. An example group, `describe` or `context`, is
12
+ # a subclass of the enclosing one; `let`, `subject` and `before` define
13
+ # methods on it, which a nested group inherits and may redefine, with
14
+ # `super` at hand; an example is an instance of a class of its own,
15
+ # built when `it` or `its` runs it, in the order of the file.
16
+ #
17
+ # @example
18
+ # require "r_spec"
19
+ #
20
+ # RSpec.describe "Name stories" do
21
+ # let(:name) { "Bob" }
22
+ #
23
+ # it { expect(name).to eq "Bob" }
24
+ #
25
+ # context "with a last name" do
26
+ # let(:name) { "#{super()} Smith" }
27
+ #
28
+ # it { expect(name).to eq "Bob Smith" }
29
+ # end
30
+ # end
31
+ #
32
+ # # spec/name_spec.rb:6: Success: expected "Bob" to eq "Bob"
33
+ # # spec/name_spec.rb:11: Success: expected "Bob Smith" to eq "Bob Smith"
10
34
  class Dsl
11
35
  BEFORE_METHOD = :initialize
36
+ # The words of an example, which a helper cannot take.
37
+ WORDS = ExpectationHelper::It
12
38
 
13
- private_constant :BEFORE_METHOD
39
+ private_constant :BEFORE_METHOD, :WORDS
14
40
 
15
- # Executes the given block before each spec in the current context runs.
41
+ # Runs the block before each example of the group, after the blocks
42
+ # of the enclosing groups and those written before it in the group,
43
+ # in the example itself: an instance variable set here is seen by the
44
+ # example. Each block is the hook of a module of its own, included in
45
+ # the group, so that the blocks chain through `super`.
16
46
  #
17
47
  # @example
18
48
  # require "r_spec"
19
49
  #
20
50
  # RSpec.describe Integer do
21
- # before do
22
- # @value = 123
23
- # end
51
+ # before { @value = 123 }
24
52
  #
25
53
  # it { expect(@value).to be 123 }
26
54
  #
27
55
  # describe "nested" do
28
- # before do
29
- # @value -= 81
30
- # end
56
+ # before { @value -= 81 }
31
57
  #
32
58
  # it { expect(@value).to be 42 }
33
59
  # end
34
- #
35
- # it { expect(@value).to be 123 }
36
60
  # end
37
61
  #
38
- # # Output to the console
39
- # # Success: expected to be 123.
40
- # # Success: expected to be 42.
41
- # # Success: expected to be 123.
62
+ # # spec/before_spec.rb:6: Success: expected 123 to be 123
63
+ # # spec/before_spec.rb:11: Success: expected 42 to be 42
42
64
  #
43
- # @param block [Proc] The content to execute at the class initialization.
65
+ # @yield the block to run before each example
66
+ # @return [nil]
67
+ # @raise [ArgumentError] without a block
44
68
  #
45
69
  # @api public
46
70
  def self.before(&block)
47
- define_method(BEFORE_METHOD) do
48
- super()
49
- instance_eval(&block)
50
- end
71
+ raise ::ArgumentError, "a block must be given" unless block
51
72
 
52
- private BEFORE_METHOD
73
+ hook = ::Module.new do
74
+ define_method(BEFORE_METHOD) do
75
+ super()
76
+ instance_eval(&block)
77
+ end
78
+ end
79
+ include hook
80
+ nil
53
81
  end
54
82
 
55
- # Sets a user-defined property.
83
+ # Defines a helper method, called each time it is used: nothing is
84
+ # memoized.
56
85
  #
57
86
  # @example
58
87
  # require "r_spec"
59
88
  #
60
- # RSpec.describe "Name stories" do
61
- # let(:name) { "Bob" }
89
+ # RSpec.describe Integer do
90
+ # let(:answer) { 42 }
62
91
  #
63
- # it { expect(name).to eq "Bob" }
92
+ # it { expect(answer).to be 42 }
64
93
  #
65
- # context "with last name" do
66
- # let(:name) { "#{super()} Smith" }
94
+ # context "when incremented" do
95
+ # let(:answer) { super().next }
67
96
  #
68
- # it { expect(name).to eq "Bob Smith" }
97
+ # it { expect(answer).to be 43 }
69
98
  # end
70
99
  # end
71
100
  #
72
- # # Output to the console
73
- # # Success: expected to eq "Bob".
74
- # # Success: expected to eq "Bob Smith".
101
+ # # spec/let_spec.rb:6: Success: expected 42 to be 42
102
+ # # spec/let_spec.rb:11: Success: expected 43 to be 43
75
103
  #
76
- # @param name [String, Symbol] The name of the property.
77
- # @param block [Proc] The content of the method to define.
78
- #
79
- # @return [Symbol] A private method that define the block content.
104
+ # @param name [Symbol, String] the name of the helper
105
+ # @yield the body of the helper, with its parameters
106
+ # @return [Symbol] the name of the helper
107
+ # @raise [Error::ReservedMethod] for the name of the hook, or a word
108
+ # of the example (`expect`, `is_expected`, a matcher), which the
109
+ # helper could not reach
80
110
  #
81
111
  # @api public
82
112
  def self.let(name, ...)
83
- raise Error::ReservedMethod if BEFORE_METHOD.equal?(name.to_sym)
113
+ name = name.to_sym
114
+ reserved = BEFORE_METHOD.equal?(name) || WORDS.method_defined?(name) || WORDS.private_method_defined?(name)
115
+ raise Error::ReservedMethod, "#{name} is reserved" if reserved
84
116
 
85
117
  private define_method(name, ...)
86
118
  end
87
119
 
88
- # Sets a user-defined property named {#subject}.
120
+ # Defines the subject, a helper named `subject`.
89
121
  #
90
122
  # @example
91
123
  # require "r_spec"
@@ -93,108 +125,96 @@ module RSpec
93
125
  # RSpec.describe Array do
94
126
  # subject { [1, 2, 3] }
95
127
  #
96
- # it "has the prescribed elements" do
97
- # expect(subject).to eq([1, 2, 3])
98
- # end
99
- #
128
+ # it { expect(subject).to eq [1, 2, 3] }
100
129
  # it { is_expected.to be_an_instance_of described_class }
101
- #
102
130
  # its(:size) { is_expected.to be 3 }
103
- # its(:downcase) { is_expected.to raise_exception NoMethodError }
104
131
  # end
105
132
  #
106
- # # Output to the console
107
- # # Success: expected to eq [1, 2, 3].
108
- # # Success: expected [1, 2, 3] to be an instance of Array.
109
- # # Success: expected to be 3.
110
- # # Success: undefined method `downcase' for [1, 2, 3]:Array.
133
+ # # spec/subject_spec.rb:6: Success: expected [1, 2, 3] to eq [1, 2, 3]
134
+ # # spec/subject_spec.rb:7: Success: expected [1, 2, 3] to be an instance of Array
135
+ # # spec/subject_spec.rb:8: Success: expected 3 to be 3
111
136
  #
112
- # @param block [Proc] The subject to set.
113
- # @return [Symbol] A {#subject} method that define the block content.
137
+ # @yield the body of the subject
138
+ # @return [Symbol] `:subject`
114
139
  #
115
140
  # @api public
116
141
  def self.subject(&)
117
142
  let(__method__, &)
118
143
  end
119
144
 
120
- # Defines an example group that describes a unit to be tested.
145
+ # Defines an example group that describes a unit to be tested. A
146
+ # module given makes `described_class` return it.
121
147
  #
122
148
  # @example
123
149
  # require "r_spec"
124
150
  #
125
151
  # RSpec.describe String do
126
- # describe "+" do
127
- # it("concats") { expect("foo" + "bar").to eq "foobar" }
152
+ # describe "#+" do
153
+ # it("concatenates") { expect("foo" + "bar").to eq "foobar" }
128
154
  # end
129
155
  # end
130
156
  #
131
- # # Output to the console
132
- # # Success: expected to eq "foobar".
157
+ # # spec/string_spec.rb:5: Success: expected "foobar" to eq "foobar"
133
158
  #
134
- # @param const [Module, String] A module to include in block context.
135
- # @param block [Proc] The block to define the specs.
159
+ # @param const [Module, String] the module under test, or a description
160
+ # @yield the specs of the group
161
+ # @return [nil]
162
+ # @raise [ArgumentError] without a block
136
163
  #
137
164
  # @api public
138
- def self.describe(const, &)
139
- desc = ::Class.new(self)
140
- desc.let(:described_class) { const } if const.is_a?(::Module)
141
- desc.instance_eval(&)
142
- end
165
+ def self.describe(const, &block)
166
+ raise ::ArgumentError, "a block must be given" unless block
143
167
 
144
- # :nocov:
168
+ group = ::Class.new(self)
169
+ group.let(:described_class) { const } if ::Module === const
170
+ group.instance_eval(&block)
171
+ nil
172
+ end
145
173
 
146
- # Defines an example group that establishes a specific context, like
147
- # _empty array_ versus _array with elements_.
148
- #
149
- # Unlike a `describe` block, all specifications executed within a
150
- # `context` are isolated in a subprocess. This prevents possible side
151
- # effects on the Ruby object environment from being propagated outside
152
- # their context, which could alter the result of the unit test suite.
174
+ # Defines an example group that establishes a context, like _empty
175
+ # array_ versus _array with elements_, and runs it in a child process:
176
+ # what its specs change in the Ruby process, a constant, a global, the
177
+ # subject itself, stays in the child. The parent exits with the status
178
+ # 1 when the child did not succeed.
153
179
  #
154
180
  # @example
155
181
  # require "r_spec"
156
182
  #
157
- # RSpec.describe "web resource" do
158
- # context "when resource is not found" do
183
+ # RSpec.describe "a web resource" do
184
+ # context "when it is not found" do
159
185
  # pending "responds with 404"
160
186
  # end
161
187
  #
162
- # context "when resource is found" do
188
+ # context "when it is found" do
163
189
  # pending "responds with 200"
164
190
  # end
165
191
  # end
166
192
  #
167
- # # Output to the console
168
- # # Warning: responds with 404.
169
- # # Warning: responds with 200.
193
+ # # spec/resource_spec.rb:5: Pending: responds with 404
194
+ # # spec/resource_spec.rb:9: Pending: responds with 200
170
195
  #
171
- # @param _description [String] A description that usually begins with
172
- # "when", "with" or "without".
173
- # @param block [Proc] The block to define the specs.
196
+ # @param _description [String] a description, usually beginning with
197
+ # "when", "with" or "without"
198
+ # @yield the specs of the context
199
+ # @return [nil]
200
+ # @raise [ArgumentError] without a block
201
+ # @raise [NotImplementedError] when this Ruby cannot fork
202
+ # @raise [SystemExit] when a spec of the context did not succeed
174
203
  #
175
204
  # @api public
176
205
  def self.context(_description, &block)
206
+ raise ::ArgumentError, "a block must be given" unless block
207
+
177
208
  pid = ::Process.fork { ::Class.new(self).instance_eval(&block) }
178
- exit false unless ::Process::Status.wait(pid).exitstatus.zero?
209
+ exit(false) unless ::Process.wait2(pid).last.success?
210
+ nil
179
211
  end
180
212
 
181
- # :nocov:
182
-
183
- # Defines a concrete test case.
184
- #
185
- # The test is performed by the block supplied to `&block`.
186
- #
187
- # @example The integer after 41
188
- # require "r_spec"
189
- #
190
- # RSpec.describe Integer do
191
- # it { expect(41.next).to be 42 }
192
- # end
213
+ # Defines a concrete test case, and runs it: the block is evaluated
214
+ # in a new example, where `expect`, `is_expected` and the matchers
215
+ # are words.
193
216
  #
194
- # # Output to the console
195
- # # Success: expected to be 42.
196
- #
197
- # @example A division by zero
217
+ # @example
198
218
  # require "r_spec"
199
219
  #
200
220
  # RSpec.describe Integer do
@@ -202,89 +222,69 @@ module RSpec
202
222
  #
203
223
  # it { is_expected.to be_an_instance_of described_class }
204
224
  #
205
- # it "raises an error" do
225
+ # it "raises on a division by zero" do
206
226
  # expect { subject / 0 }.to raise_exception ZeroDivisionError
207
227
  # end
208
228
  # end
209
229
  #
210
- # # Output to the console
211
- # # Success: expected 41 to be an instance of Integer.
212
- # # Success: divided by 0.
213
- #
214
- # It can be used inside a {.describe} or {.context} section.
215
- #
216
- # @param _name [String, nil] The name of the spec.
217
- # @param block [Proc] An expectation to evaluate.
230
+ # # spec/integer_spec.rb:6: Success: expected 41 to be an instance of Integer
231
+ # # spec/integer_spec.rb:9: Success: expected #<ZeroDivisionError: divided by 0> to raise exception ZeroDivisionError
218
232
  #
219
- # @raise (see ExpectationTarget::Base#result)
220
- # @return (see ExpectationTarget::Base#result)
233
+ # @param _name [String, nil] the name of the example
234
+ # @yield the example, with its expectations
235
+ # @return [nil]
236
+ # @raise [ArgumentError] without a block
237
+ # @raise [SystemExit] when an expectation is not met
221
238
  #
222
239
  # @api public
223
240
  def self.it(_name = nil, &block)
224
- Logger.source(*block.source_location)
225
- example_without_attribute.new.instance_eval(&block)
226
- end
241
+ raise ::ArgumentError, "a block must be given" unless block
227
242
 
228
- # :nocov:
243
+ ::Class.new(self) { prepend ExpectationHelper::It }.new.instance_eval(&block)
244
+ nil
245
+ end
229
246
 
230
- # Defines a single concrete test case that specifies the actual value of
231
- # an attribute of the subject using {ExpectationHelper::Its#is_expected}.
247
+ # Defines a concrete test case about an attribute of the subject, and
248
+ # runs it: `is_expected`, the one word of the example with the
249
+ # matchers, wraps the attribute, sent to the subject with the
250
+ # arguments when the matcher asks for it, so that what it raises can
251
+ # be expected too.
232
252
  #
233
- # @example The integer after 41
253
+ # @example
234
254
  # require "r_spec"
235
255
  #
236
256
  # RSpec.describe Integer do
237
257
  # subject { 41 }
238
258
  #
239
259
  # its(:next) { is_expected.to be 42 }
240
- # end
241
- #
242
- # # Output to the console
243
- # # Success: expected to be 42.
244
- #
245
- # @example A division by zero
246
- # require "r_spec"
247
- #
248
- # RSpec.describe Integer do
249
- # subject { 41 }
250
- #
251
260
  # its(:/, 0) { is_expected.to raise_exception ZeroDivisionError }
252
261
  # end
253
262
  #
254
- # # Output to the console
255
- # # Success: divided by 0.
256
- #
257
- # @example A spec without subject
258
- # require "r_spec"
259
- #
260
- # RSpec.describe Integer do
261
- # its(:abs) { is_expected.to raise_exception RSpec::Clone::Error::UndefinedSubject }
262
- # end
263
- #
264
- # # Output to the console
265
- # # Success: subject not explicitly defined.
266
- #
267
- # @param attribute [String, Symbol] The property to call to subject.
268
- # @param args [Array] An optional list of arguments.
269
- # @param kwargs [Hash] An optional list of keyword arguments.
270
- # @param block [Proc] An expectation to evaluate.
263
+ # # spec/its_spec.rb:6: Success: expected 42 to be 42
264
+ # # spec/its_spec.rb:7: Success: expected #<ZeroDivisionError: divided by 0> to raise exception ZeroDivisionError
271
265
  #
272
- # @raise (see ExpectationTarget::Base#result)
273
- # @return (see ExpectationTarget::Base#result)
266
+ # @param attribute [Symbol] the method to send to the subject, followed
267
+ # by its arguments
268
+ # @yield the example, with its expectation
269
+ # @return [nil]
270
+ # @raise [ArgumentError] without a block, or when the attribute is not
271
+ # a Symbol
272
+ # @raise [SystemExit] when the expectation is not met
274
273
  #
275
274
  # @api public
276
275
  def self.its(attribute, *, **, &block)
277
- Logger.source(*block.source_location)
278
- example_with_attribute(attribute, *, **).new
279
- .instance_eval(&block)
280
- end
276
+ raise ::ArgumentError, "a block must be given" unless block
281
277
 
282
- # :nocov:
278
+ challenge = ::Defi::Challenge.new(attribute, *, **)
279
+ example = ::Class.new(self) do
280
+ prepend ExpectationHelper::Matcher
281
+ define_method(:is_expected) { ExpectationTarget.new(-> { challenge.to(subject).call }) }
282
+ end
283
+ example.new.instance_eval(&block)
284
+ nil
285
+ end
283
286
 
284
- # Defines a pending test case.
285
- #
286
- # `&block` is never evaluated. It can be used to describe behaviour that
287
- # is not yet implemented.
287
+ # Defines a pending test case: its block, if any, is never run.
288
288
  #
289
289
  # @example
290
290
  # require "r_spec"
@@ -294,53 +294,29 @@ module RSpec
294
294
  # expect something to be finished
295
295
  # end
296
296
  #
297
- # pending "is not yet implemented and waiting"
297
+ # pending "is not yet implemented"
298
298
  # end
299
299
  #
300
- # # Output to the console
301
- # # Warning: is implemented but waiting.
302
- # # Warning: is not yet implemented and waiting.
300
+ # # spec/pending_spec.rb:4: Pending: is implemented but waiting
301
+ # # spec/pending_spec.rb:8: Pending: is not yet implemented
303
302
  #
304
- # @param message [String] The reason why the example is pending.
305
- #
306
- # @return [nil] Write a message to STDOUT.
303
+ # @param message [String] why the example is pending
304
+ # @yield never: the block is not run
305
+ # @return [nil]
307
306
  #
308
307
  # @api public
309
- def self.pending(message)
310
- Logger.passed_spec Error::PendingExpectation.result(message)
311
- end
312
-
313
- # Example class for concrete test case.
314
- def self.example_without_attribute
315
- ::Class.new(self) do
316
- prepend ExpectationHelper::It
317
- end
308
+ def self.pending(message, &)
309
+ Report.pending(Report.location, message)
318
310
  end
319
311
 
320
- # Example class for concrete test case that specifies the actual value of
321
- # an attribute of the subject.
322
- def self.example_with_attribute(attribute, *args, **kwargs)
323
- ::Class.new(self) do
324
- prepend ExpectationHelper::Its
325
-
326
- define_method(:actual) do
327
- subject.public_send(attribute, *args, **kwargs)
328
- end
329
- end
330
- end
331
-
332
- private_class_method :example_without_attribute, :example_with_attribute
333
-
334
312
  private
335
313
 
336
- # If the first argument to a {.describe} definition is a class (or a
337
- # module), this method will be overridden to return it.
314
+ # Overridden by `describe` when it is given a module.
338
315
  def described_class
339
- raise Error::UndefinedDescribedClass,
340
- "the first argument to at least one example group must be a module"
316
+ raise Error::UndefinedDescribedClass, "the first argument to at least one example group must be a module"
341
317
  end
342
318
 
343
- # If a subject is defined, this method will be overridden to return it.
319
+ # Overridden by `subject`.
344
320
  def subject
345
321
  raise Error::UndefinedSubject, "subject not explicitly defined"
346
322
  end
@@ -1,14 +1,22 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative File.join("error", "pending_expectation")
4
- require_relative File.join("error", "reserved_method")
5
- require_relative File.join("error", "undefined_described_class")
6
- require_relative File.join("error", "undefined_subject")
7
-
8
3
  module RSpec
9
4
  module Clone
10
- # Namespace for exceptions.
5
+ # The errors of the clone, raised where a spec asks for what it did not
6
+ # define.
11
7
  module Error
8
+ # Raised by `let` for a name the clone keeps for itself.
9
+ class ReservedMethod < ::RuntimeError
10
+ end
11
+
12
+ # Raised by `described_class` when no enclosing `describe` names a
13
+ # module.
14
+ class UndefinedDescribedClass < ::RuntimeError
15
+ end
16
+
17
+ # Raised by `subject` when no enclosing group defines one.
18
+ class UndefinedSubject < ::RuntimeError
19
+ end
12
20
  end
13
21
  end
14
22
  end
@@ -1,39 +1,37 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "shared"
3
+ require_relative "matcher"
4
4
  require_relative File.join("..", "expectation_target")
5
5
 
6
6
  module RSpec
7
7
  module Clone
8
8
  module ExpectationHelper
9
- # {RSpec::Clone::Dsl.it}'s expectation helper module.
9
+ # The words of an `it` example: `expect`, `is_expected` and the
10
+ # matchers.
10
11
  module It
11
- include Shared
12
+ include Matcher
12
13
 
13
- # Create an expectation for a spec.
14
- #
15
- # @param value [#object_id, nil] An actual value.
16
- # @param block [#call, nil] A code to evaluate.
17
- #
18
- # @return [Block, Value] The wrapped target of an expectation.
14
+ # Wraps the target of an expectation: a value, or a block whose
15
+ # value or exception the matcher judges.
19
16
  #
20
17
  # @example
21
- # expect("foo") # => #<RSpec::Clone::ExpectationTarget::Value:0x00007f @actual="foo">
22
- # expect { RSpec::Clone::Boom! } # => #<RSpec::Clone::ExpectationTarget::Block:0x... @callable=#<Proc:0x...>>
18
+ # expect("foo")
19
+ # expect { 1 / 0 }
23
20
  #
24
- # @api public
25
- def expect(value = self.class.superclass, &block)
26
- ExpectationTarget.call(self.class.superclass, value, block)
21
+ # @param values [Array] the actual value, when no block is given
22
+ # @yieldreturn [Object] the actual value
23
+ # @return [ExpectationTarget]
24
+ # @raise [ArgumentError] with both a value and a block, or neither
25
+ def expect(*values, &block)
26
+ raise ::ArgumentError, "expect takes one value or a block" unless values.size + (block ? 1 : 0) == 1
27
+
28
+ ExpectationTarget.new(block || -> { values.fetch(0) })
27
29
  end
28
30
 
29
- # Wraps the target of an expectation with the subject as actual value.
30
- #
31
- # @return [Block] The wrapped target of an expectation.
32
- #
33
- # @example
34
- # is_expected # => #<RSpec::Clone::ExpectationTarget::Block:0x00007fb6b8 @callable=#<Proc:0x00007fb6b8>>
31
+ # Wraps the subject as a block: `is_expected.to raise_exception X`
32
+ # judges what `subject` raises.
35
33
  #
36
- # @api public
34
+ # @return [ExpectationTarget]
37
35
  def is_expected
38
36
  expect { subject }
39
37
  end