crspec 0.1.2 → 0.1.3

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.
@@ -4,22 +4,28 @@ require_relative "expectations"
4
4
  require_relative "example"
5
5
  require_relative "mock/double"
6
6
  require_relative "configuration"
7
+ require_relative "file_fixtures"
8
+ require_relative "shared_examples"
7
9
 
8
10
  module Crspec
9
11
  class ExampleGroup
10
12
  include Expectations
11
13
  include Mock::DSL
14
+ include SharedDSL
12
15
 
13
- attr_reader :description, :metadata, :parent, :examples, :children, :before_hooks, :after_hooks
16
+ attr_reader :description, :metadata, :parent, :examples, :children, :before_hooks, :after_hooks, :around_hooks
17
+ attr_accessor :described_module
14
18
 
15
19
  def initialize(description, metadata = {}, parent = nil)
16
20
  @description = description
21
+ @described_module = description.is_a?(Module) ? description : nil
17
22
  @metadata = metadata.freeze
18
23
  @parent = parent
19
24
  @examples = []
20
25
  @children = []
21
26
  @before_hooks = []
22
27
  @after_hooks = []
28
+ @around_hooks = []
23
29
  @let_blocks = {}
24
30
  @eager_lets = []
25
31
  @included_modules = []
@@ -31,24 +37,111 @@ module Crspec
31
37
  group
32
38
  end
33
39
 
34
- def describe(description, metadata = {}, &block)
35
- child = self.class.define(description, metadata, self, &block)
40
+ def describe(*args, **kwargs, &block)
41
+ metadata = kwargs.dup
42
+ metadata.merge!(args.pop) if args.last.is_a?(Hash)
43
+ described = args.first.is_a?(Module) ? args.first : nil
44
+ description = described && args.size == 1 ? described : args.map(&:to_s).join(" ")
45
+ child = self.class.new(description, metadata, self)
46
+ child.described_module = described
47
+ child.instance_eval(&block) if block
36
48
  @children << child
37
49
  child
38
50
  end
39
51
  alias context describe
40
52
 
53
+ def xdescribe(*args, **kwargs, &block)
54
+ child = describe(*args, **kwargs, &block)
55
+ child.mark_pending!
56
+ child
57
+ end
58
+ alias xcontext xdescribe
59
+
60
+ def fdescribe(*args, **kwargs, &block)
61
+ child = describe(*args, **kwargs, &block)
62
+ child.mark_focused!
63
+ child
64
+ end
65
+ alias fcontext fdescribe
66
+
41
67
  def it(description = nil, metadata = {}, &block)
42
- example = Example.new(description || "unnamed example", metadata, self, block)
68
+ example = Example.new(description || "unnamed example", metadata, self, block,
69
+ pending: pending?)
43
70
  @examples << example
44
71
  example
45
72
  end
46
73
  alias specify it
47
74
 
75
+ def xit(description = nil, metadata = {}, &block)
76
+ example = Example.new(description || "unnamed example", metadata, self, block,
77
+ pending: true)
78
+ @examples << example
79
+ example
80
+ end
81
+ alias pending xit
82
+ alias skip xit
83
+
84
+ def fit(description = nil, metadata = {}, &block)
85
+ example = it(description, metadata, &block)
86
+ example.focused = true
87
+ example
88
+ end
89
+
90
+ def mark_pending!
91
+ @pending = true
92
+ @examples.each { |ex| ex.instance_variable_set(:@pending, true) }
93
+ @children.each(&:mark_pending!)
94
+ self
95
+ end
96
+
97
+ def pending?
98
+ !!@pending || (parent ? parent.pending? : false)
99
+ end
100
+
101
+ def mark_focused!
102
+ @focused = true
103
+ @examples.each { |ex| ex.focused = true }
104
+ @children.each(&:mark_focused!)
105
+ self
106
+ end
107
+
108
+ def focused?
109
+ !!@focused
110
+ end
111
+
48
112
  def include(mod)
49
113
  @included_modules << mod
50
114
  end
51
115
 
116
+ def render_views(*args)
117
+ end
118
+
119
+ def private(*args)
120
+ end
121
+
122
+ def public(*args)
123
+ end
124
+
125
+ def protected(*args)
126
+ end
127
+
128
+ def attr_reader(*names)
129
+ names.each do |name|
130
+ let(name) { instance_variable_get("@#{name}") }
131
+ end
132
+ end
133
+
134
+ def attr_writer(*names)
135
+ names.each do |name|
136
+ define_method("#{name}=") { |val| instance_variable_set("@#{name}", val) }
137
+ end
138
+ end
139
+
140
+ def attr_accessor(*names)
141
+ attr_reader(*names)
142
+ attr_writer(*names)
143
+ end
144
+
52
145
  def let(name, &block)
53
146
  @let_blocks[name.to_sym] = block
54
147
  end
@@ -67,79 +160,276 @@ module Crspec
67
160
  end
68
161
  end
69
162
 
70
- def before(_scope = :each, &block)
71
- @before_hooks << block
163
+ FORBIDDEN_HOOK_SCOPES = %i[all context suite].freeze
164
+
165
+ def before(*args, **kwargs, &block)
166
+ reject_serial_scope!(:before, args)
167
+ @before_hooks << { block: block, filters: args, kwfilters: kwargs }
72
168
  end
73
169
 
74
- def after(_scope = :each, &block)
75
- @after_hooks.unshift(block)
170
+ def after(*args, **kwargs, &block)
171
+ reject_serial_scope!(:after, args)
172
+ @after_hooks.unshift({ block: block, filters: args, kwfilters: kwargs })
76
173
  end
77
174
 
78
- def ancestor_hooks(type)
79
- hooks = []
80
- hooks.concat(Crspec.configuration.before_hooks) if type == :before
175
+ def reject_serial_scope!(hook_name, args)
176
+ scope = args.first
177
+ return unless scope.is_a?(Symbol) && FORBIDDEN_HOOK_SCOPES.include?(scope)
81
178
 
82
- curr = self
83
- ancestors = []
84
- while curr
85
- ancestors.unshift(curr)
86
- curr = curr.parent
87
- end
179
+ raise MigrationError, <<~MSG
180
+ #{hook_name}(:#{scope}) is not supported by Crspec: shared state across
181
+ examples is inherently serial and breaks the P processes x N threads x
182
+ M fibers execution model.
88
183
 
89
- ancestors.each do |grp|
90
- list = type == :before ? grp.before_hooks : grp.after_hooks
91
- hooks.concat(list)
184
+ Migrate to per-example setup instead:
185
+ - use before(:each) if the setup is cheap
186
+ - use let/let! for lazily-built (memoized per example) values
187
+
188
+ Run `crspec-transpile --analyze` on your suite to find and rewrite all
189
+ occurrences automatically.
190
+ MSG
191
+ end
192
+
193
+ def around(*args, **kwargs, &block)
194
+ @around_hooks << { block: block, filters: args, kwfilters: kwargs }
195
+ end
196
+
197
+ def finalize!
198
+ ancestor_metadata
199
+ ancestor_let_blocks
200
+ ancestor_eager_lets
201
+ ancestor_included_modules
202
+ described_class
203
+ %i[before after around].each { |type| raw_hooks_for(type) }
204
+
205
+ example_types = examples.map { |ex| effective_type_for(ex) }.uniq
206
+ example_types.each do |type|
207
+ %i[before after around].each { |hook_type| filtered_hooks(hook_type, type) }
208
+ modules_for_type(type)
209
+ example_class_for(type)
92
210
  end
93
211
 
94
- hooks.concat(Crspec.configuration.after_hooks) if type == :after
212
+ children.each(&:finalize!)
213
+ self
214
+ end
95
215
 
96
- hooks
216
+ def ancestor_hooks(type, example = nil)
217
+ filtered_hooks(type, example ? effective_type_for(example) : ancestor_metadata[:type])
97
218
  end
98
219
 
99
220
  def ancestor_let_blocks
100
- blocks = {}
101
- curr = self
102
- ancestors = []
103
- while curr
104
- ancestors.unshift(curr)
105
- curr = curr.parent
106
- end
107
- ancestors.each do |grp|
108
- blocks.merge!(grp.instance_variable_get(:@let_blocks))
221
+ @ancestor_let_blocks ||= begin
222
+ blocks = {}
223
+ ancestor_chain.each do |grp|
224
+ blocks.merge!(grp.instance_variable_get(:@let_blocks))
225
+ end
226
+ unless blocks.key?(:subject)
227
+ desc_cls = described_class
228
+ if desc_cls.is_a?(Class)
229
+ blocks[:subject] = proc { desc_cls.new rescue nil }
230
+ elsif desc_cls
231
+ blocks[:subject] = proc { desc_cls }
232
+ end
233
+ end
234
+ blocks.freeze
109
235
  end
110
- blocks
111
236
  end
112
237
 
113
238
  def ancestor_eager_lets
114
- eager = []
115
- curr = self
116
- while curr
117
- eager.concat(curr.instance_variable_get(:@eager_lets))
118
- curr = curr.parent
119
- end
120
- eager
239
+ @ancestor_eager_lets ||= ancestor_chain.flat_map do |grp|
240
+ grp.instance_variable_get(:@eager_lets)
241
+ end.freeze
121
242
  end
122
243
 
123
244
  def ancestor_included_modules
124
- mods = []
125
- curr = self
126
- ancestors = []
127
- while curr
128
- ancestors.unshift(curr)
129
- curr = curr.parent
245
+ @ancestor_included_modules ||= ancestor_chain.flat_map do |grp|
246
+ grp.instance_variable_get(:@included_modules) || []
247
+ end.freeze
248
+ end
249
+
250
+ def ancestor_metadata
251
+ @ancestor_metadata ||= begin
252
+ meta = {}
253
+ ancestor_chain.each do |grp|
254
+ meta.merge!(grp.metadata || {})
255
+ end
256
+ meta.freeze
130
257
  end
131
- ancestors.each do |grp|
132
- mods.concat(grp.instance_variable_get(:@included_modules) || [])
258
+ end
259
+
260
+ def modules_for_example(example)
261
+ modules_for_type(effective_type_for(example))
262
+ end
263
+
264
+ def described_class
265
+ return @described_class if defined?(@described_class)
266
+
267
+ @described_class = begin
268
+ found = nil
269
+ curr = self
270
+ while curr
271
+ if curr.described_module
272
+ found = curr.described_module
273
+ break
274
+ end
275
+ curr = curr.parent
276
+ end
277
+ found
278
+ end
279
+ end
280
+
281
+ def effective_type_for(example)
282
+ meta = example.metadata || {}
283
+ meta.key?(:type) ? meta[:type] : ancestor_metadata[:type]
284
+ end
285
+
286
+ def run_eager_lets(instance)
287
+ ancestor_eager_lets.each do |name|
288
+ instance.send(name)
133
289
  end
134
- mods
135
290
  end
136
291
 
137
292
  def create_instance(example)
138
- modules_to_include = (Crspec.configuration.included_modules + ancestor_included_modules).uniq
293
+ klass = example_class_for(effective_type_for(example))
294
+ inst = klass.new(example)
295
+
296
+ klass.setup_callbacks.each do |cb|
297
+ if cb.is_a?(Symbol) || cb.is_a?(String)
298
+ inst.send(cb)
299
+ elsif cb.respond_to?(:call)
300
+ inst.instance_exec(&cb)
301
+ end
302
+ end
303
+
304
+ inst
305
+ end
306
+
307
+ private
308
+
309
+ def ancestor_chain
310
+ @ancestor_chain ||= begin
311
+ chain = []
312
+ curr = self
313
+ while curr
314
+ chain.unshift(curr)
315
+ curr = curr.parent
316
+ end
317
+ chain.freeze
318
+ end
319
+ end
320
+
321
+ def raw_hooks_for(type)
322
+ @raw_hooks_for ||= {}
323
+ @raw_hooks_for[type] ||= begin
324
+ raw_hooks = []
325
+ if type == :before
326
+ raw_hooks.concat(Crspec.configuration.before_hooks)
327
+ elsif type == :around
328
+ raw_hooks.concat(Crspec.configuration.around_hooks)
329
+ end
330
+
331
+ ancestor_chain.each do |grp|
332
+ list = case type
333
+ when :before then grp.before_hooks
334
+ when :after then grp.after_hooks
335
+ when :around then grp.around_hooks
336
+ end
337
+ raw_hooks.concat(list)
338
+ end
339
+
340
+ raw_hooks.concat(Crspec.configuration.after_hooks) if type == :after
341
+ raw_hooks.freeze
342
+ end
343
+ end
344
+
345
+ def filtered_hooks(hook_type, meta_type)
346
+ @filtered_hooks ||= {}
347
+ @filtered_hooks[[hook_type, meta_type]] ||= raw_hooks_for(hook_type).filter_map do |hook|
348
+ if hook.is_a?(Hash)
349
+ blk = hook[:block]
350
+ kw = hook[:kwfilters] || {}
351
+ filters = hook[:filters] || []
352
+
353
+ target_type = kw[:type] || (filters.first.is_a?(Hash) && filters.first[:type])
354
+ next if target_type && meta_type != target_type
355
+
356
+ blk
357
+ else
358
+ hook
359
+ end
360
+ end.freeze
361
+ end
362
+
363
+ def modules_for_type(meta_type)
364
+ @modules_for_type ||= {}
365
+ @modules_for_type[meta_type] ||= begin
366
+ all_entries = Crspec.configuration.included_modules + ancestor_included_modules
367
+
368
+ all_entries.filter_map do |entry|
369
+ if entry.is_a?(Hash)
370
+ mod = entry[:module]
371
+ kw = entry[:kwfilters] || {}
372
+ filters = entry[:filters] || []
373
+
374
+ target_type = kw[:type] || (filters.first.is_a?(Hash) && filters.first[:type])
375
+ next if target_type && meta_type != target_type
376
+
377
+ mod
378
+ else
379
+ entry
380
+ end
381
+ end.uniq.freeze
382
+ end
383
+ end
384
+
385
+ def example_class_for(meta_type)
386
+ @example_class_for ||= {}
387
+ @example_class_for[meta_type] ||= build_example_class(meta_type)
388
+ end
389
+
390
+ def build_example_class(meta_type)
391
+ modules_to_include = modules_for_type(meta_type)
392
+ desc_cls = described_class
139
393
 
140
394
  klass = Class.new do
395
+ define_singleton_method(:spec_type) { meta_type }
396
+
397
+ class << self
398
+ attr_writer :controller_class
399
+
400
+ def controller_class
401
+ @controller_class
402
+ end
403
+
404
+ def setup(*args, &block)
405
+ @setup_callbacks ||= []
406
+ @setup_callbacks << (block || args.first)
407
+ end
408
+
409
+ def teardown(*args, &block)
410
+ @teardown_callbacks ||= []
411
+ @teardown_callbacks << (block || args.first)
412
+ end
413
+
414
+ def setup_callbacks
415
+ @setup_callbacks || []
416
+ end
417
+
418
+ def teardown_callbacks
419
+ @teardown_callbacks || []
420
+ end
421
+ end
422
+
141
423
  include Expectations
142
424
  include Mock::DSL
425
+ include FileFixtures
426
+
427
+ if meta_type == :controller
428
+ include ActionController::TestCase::Behavior if defined?(ActionController::TestCase::Behavior)
429
+ elsif meta_type == :request
430
+ include ActionDispatch::Integration::Runner if defined?(ActionDispatch::Integration::Runner)
431
+ include ActionDispatch::IntegrationTest::Behavior if defined?(ActionDispatch::IntegrationTest::Behavior)
432
+ end
143
433
 
144
434
  modules_to_include.each do |mod|
145
435
  include mod
@@ -147,8 +437,25 @@ module Crspec
147
437
 
148
438
  attr_reader :__crspec_example__
149
439
 
150
- def initialize(example)
440
+ define_method(:initialize) do |example|
151
441
  @__crspec_example__ = example
442
+ meta_type = self.class.spec_type
443
+ is_controller = meta_type == :controller || (desc_cls.is_a?(Class) && desc_cls.ancestors.map(&:to_s).include?("ActionController::Metal"))
444
+ if is_controller
445
+ ctrl_cls = desc_cls
446
+ begin
447
+ @controller = ctrl_cls.new if ctrl_cls.is_a?(Class)
448
+ rescue StandardError
449
+ # ignore
450
+ end
451
+ if defined?(ActionDispatch::TestRequest)
452
+ @request = ActionDispatch::TestRequest.create rescue nil
453
+ @response = ActionDispatch::TestResponse.new rescue nil
454
+ end
455
+ if defined?(::Rails) && ::Rails.application
456
+ @routes = ::Rails.application.routes
457
+ end
458
+ end
152
459
  end
153
460
 
154
461
  def execution_context
@@ -156,13 +463,14 @@ module Crspec
156
463
  end
157
464
  alias current_context execution_context
158
465
 
159
- def described_class
160
- nil
466
+ define_method(:described_class) do
467
+ desc_cls
161
468
  end
162
469
  end
163
470
 
164
- lets = ancestor_let_blocks
165
- lets.each do |let_name, let_proc|
471
+ klass.controller_class = desc_cls if desc_cls.is_a?(Class)
472
+
473
+ ancestor_let_blocks.each do |let_name, let_proc|
166
474
  current_proc = let_proc
167
475
  klass.define_method(let_name) do
168
476
  ExecutionContext.current.fetch_memoized(let_name) do
@@ -171,15 +479,7 @@ module Crspec
171
479
  end
172
480
  end
173
481
 
174
- inst = klass.new(example)
175
-
176
- # Evaluate eager lets
177
- eager_lets = ancestor_eager_lets
178
- eager_lets.each do |name|
179
- inst.send(name)
180
- end
181
-
182
- inst
482
+ klass
183
483
  end
184
484
  end
185
485
  end
@@ -14,13 +14,17 @@ module Crspec
14
14
  end
15
15
 
16
16
  def self.isolate(example_id, metadata = {})
17
- parent_context = Fiber[STORAGE_KEY]
18
- new_context = new(example_id, metadata, parent_context)
19
-
20
- # Fiber storage inheritance preserves context while isolating mutations
21
- Fiber.new(storage: { STORAGE_KEY => new_context }) do
17
+ new_context = new(example_id, metadata, Fiber[STORAGE_KEY])
18
+ old_context = Fiber[STORAGE_KEY]
19
+ old_space = Fiber[Mock::Space::STORAGE_KEY]
20
+ Fiber[STORAGE_KEY] = new_context
21
+ Fiber[Mock::Space::STORAGE_KEY] = nil
22
+ begin
22
23
  yield new_context
23
- end.resume
24
+ ensure
25
+ Fiber[STORAGE_KEY] = old_context
26
+ Fiber[Mock::Space::STORAGE_KEY] = old_space
27
+ end
24
28
  end
25
29
 
26
30
  def initialize(example_id, metadata = {}, parent = nil)
@@ -5,6 +5,42 @@ require_relative "matchers"
5
5
  module Crspec
6
6
  class ExpectationNotMetError < StandardError; end
7
7
 
8
+ class MultipleExpectationsNotMetError < ExpectationNotMetError
9
+ attr_reader :failures
10
+
11
+ def initialize(failures)
12
+ @failures = failures
13
+ summary = failures.each_with_index.map { |msg, i| " #{i + 1}) #{msg}" }.join("\n")
14
+ super("Got #{failures.size} failure(s):\n#{summary}")
15
+ end
16
+ end
17
+
18
+ # Fiber-local failure accumulator backing aggregate_failures.
19
+ module FailureAggregator
20
+ STORAGE_KEY = :crspec_failure_aggregator
21
+
22
+ def self.active?
23
+ !Fiber[STORAGE_KEY].nil?
24
+ end
25
+
26
+ def self.record(message)
27
+ Fiber[STORAGE_KEY] << message
28
+ end
29
+
30
+ def self.aggregate
31
+ previous = Fiber[STORAGE_KEY]
32
+ Fiber[STORAGE_KEY] = []
33
+ begin
34
+ yield
35
+ failures = Fiber[STORAGE_KEY]
36
+ raise MultipleExpectationsNotMetError, failures if failures.size > 1
37
+ raise ExpectationNotMetError, failures.first if failures.size == 1
38
+ ensure
39
+ Fiber[STORAGE_KEY] = previous
40
+ end
41
+ end
42
+ end
43
+
8
44
  class ExpectationTarget
9
45
  def initialize(actual)
10
46
  @actual = actual
@@ -14,7 +50,10 @@ module Crspec
14
50
  matcher ||= block
15
51
  return matcher.setup_expect(@actual) if matcher.respond_to?(:setup_expect)
16
52
 
17
- raise ExpectationNotMetError, matcher.failure_message unless matcher.matches?(@actual)
53
+ unless matcher.matches?(@actual)
54
+ fail_with(matcher.failure_message)
55
+ return false
56
+ end
18
57
 
19
58
  true
20
59
  end
@@ -23,22 +62,43 @@ module Crspec
23
62
  matcher ||= block
24
63
  if matcher.matches?(@actual)
25
64
  msg = matcher.respond_to?(:failure_message_when_negated) ? matcher.failure_message_when_negated : "Expected matcher not to match"
26
- raise ExpectationNotMetError, msg
65
+ fail_with(msg)
66
+ return false
27
67
  end
28
68
  true
29
69
  end
30
70
 
31
71
  alias to_not not_to
72
+
73
+ private
74
+
75
+ def fail_with(message)
76
+ if FailureAggregator.active?
77
+ FailureAggregator.record(message)
78
+ else
79
+ raise ExpectationNotMetError, message
80
+ end
81
+ end
32
82
  end
33
83
 
34
84
  module Expectations
35
85
  include Matchers
36
86
 
87
+ ExpectationNotMetError = Crspec::ExpectationNotMetError
88
+
37
89
  UNDEFINED = Object.new.freeze
38
90
 
39
91
  def expect(value = UNDEFINED, &block)
40
92
  actual = value.equal?(UNDEFINED) ? block : value
41
93
  ExpectationTarget.new(actual)
42
94
  end
95
+
96
+ def is_expected
97
+ expect(subject)
98
+ end
99
+
100
+ def aggregate_failures(_label = nil, &block)
101
+ FailureAggregator.aggregate(&block)
102
+ end
43
103
  end
44
104
  end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pathname"
4
+
5
+ module Crspec
6
+ module FileFixtures
7
+ def file_fixture_path
8
+ Crspec.configuration.file_fixture_path
9
+ end
10
+
11
+ def file_fixture(fixture_name)
12
+ path = Pathname.new(File.join(file_fixture_path, fixture_name))
13
+ return path if path.file?
14
+
15
+ raise ArgumentError, "the directory '#{file_fixture_path}' does not contain a file named '#{fixture_name}'"
16
+ end
17
+ end
18
+ end