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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +86 -0
- data/README.md +1 -1
- data/lib/crspec/cli.rb +86 -5
- data/lib/crspec/configuration.rb +62 -9
- data/lib/crspec/dsl.rb +66 -2
- data/lib/crspec/example.rb +60 -6
- data/lib/crspec/example_group.rb +363 -63
- data/lib/crspec/execution_context.rb +10 -6
- data/lib/crspec/expectations.rb +62 -2
- data/lib/crspec/file_fixtures.rb +18 -0
- data/lib/crspec/formatters/progress_formatter.rb +37 -14
- data/lib/crspec/matchers.rb +642 -0
- data/lib/crspec/mock/argument_matchers.rb +157 -0
- data/lib/crspec/mock/double.rb +246 -13
- data/lib/crspec/mock/interceptor.rb +32 -9
- data/lib/crspec/mock/space.rb +14 -0
- data/lib/crspec/process_runner.rb +332 -0
- data/lib/crspec/rails/assets_shim.rb +33 -0
- data/lib/crspec/rails/database_isolation.rb +179 -8
- data/lib/crspec/rails/parallel.rb +29 -29
- data/lib/crspec/rails/request_helpers.rb +70 -14
- data/lib/crspec/rails/system_server.rb +7 -0
- data/lib/crspec/rails/warden_shim.rb +18 -0
- data/lib/crspec/runner.rb +143 -29
- data/lib/crspec/shared_examples.rb +70 -0
- data/lib/crspec/status_persistence.rb +46 -0
- data/lib/crspec/transpiler/cli.rb +106 -15
- data/lib/crspec/transpiler/rewriter.rb +179 -25
- data/lib/crspec/version.rb +1 -1
- data/lib/crspec.rb +6 -0
- data/lib/rspec/core.rb +2 -0
- data/lib/rspec/expectations.rb +2 -0
- data/lib/rspec/mocks.rb +2 -0
- data/lib/rspec/rails.rb +2 -0
- data/lib/rspec.rb +2 -0
- metadata +27 -1
data/lib/crspec/matchers.rb
CHANGED
|
@@ -16,6 +16,56 @@ module Crspec
|
|
|
16
16
|
def failure_message_when_negated
|
|
17
17
|
"Expected #{@actual.inspect} not to match #{@expected.inspect}"
|
|
18
18
|
end
|
|
19
|
+
|
|
20
|
+
def and(other)
|
|
21
|
+
CompoundAndMatcher.new(self, other)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def or(other)
|
|
25
|
+
CompoundOrMatcher.new(self, other)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
class CompoundAndMatcher
|
|
30
|
+
def initialize(first, second)
|
|
31
|
+
@first = first
|
|
32
|
+
@second = second
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def matches?(actual)
|
|
36
|
+
@first_matched = @first.matches?(actual)
|
|
37
|
+
@second_matched = @second.matches?(actual)
|
|
38
|
+
@first_matched && @second_matched
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def failure_message
|
|
42
|
+
messages = []
|
|
43
|
+
messages << @first.failure_message unless @first_matched
|
|
44
|
+
messages << @second.failure_message unless @second_matched
|
|
45
|
+
messages.join("\n...and:\n")
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def failure_message_when_negated
|
|
49
|
+
"Expected compound matcher not to match"
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def and(other)
|
|
53
|
+
CompoundAndMatcher.new(self, other)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def or(other)
|
|
57
|
+
CompoundOrMatcher.new(self, other)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
class CompoundOrMatcher < CompoundAndMatcher
|
|
62
|
+
def matches?(actual)
|
|
63
|
+
@first.matches?(actual) || @second.matches?(actual)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def failure_message
|
|
67
|
+
"#{@first.failure_message}\n...or:\n#{@second.failure_message}"
|
|
68
|
+
end
|
|
19
69
|
end
|
|
20
70
|
|
|
21
71
|
class EqMatcher < BaseMatcher
|
|
@@ -87,6 +137,36 @@ module Crspec
|
|
|
87
137
|
end
|
|
88
138
|
end
|
|
89
139
|
|
|
140
|
+
class BeAKindOfMatcher < BaseMatcher
|
|
141
|
+
def matches?(actual)
|
|
142
|
+
@actual = actual
|
|
143
|
+
@actual.is_a?(@expected)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def failure_message
|
|
147
|
+
"Expected #{@actual.inspect} to be a kind of #{@expected}"
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def failure_message_when_negated
|
|
151
|
+
"Expected #{@actual.inspect} not to be a kind of #{@expected}"
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
class BeAnInstanceOfMatcher < BaseMatcher
|
|
156
|
+
def matches?(actual)
|
|
157
|
+
@actual = actual
|
|
158
|
+
@actual.instance_of?(@expected)
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def failure_message
|
|
162
|
+
"Expected #{@actual.inspect} to be an instance of #{@expected}, but was #{@actual.class}"
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def failure_message_when_negated
|
|
166
|
+
"Expected #{@actual.inspect} not to be an instance of #{@expected}"
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
|
|
90
170
|
class IncludeMatcher < BaseMatcher
|
|
91
171
|
def matches?(actual)
|
|
92
172
|
@actual = actual
|
|
@@ -219,6 +299,478 @@ module Crspec
|
|
|
219
299
|
end
|
|
220
300
|
end
|
|
221
301
|
|
|
302
|
+
class PredicateMatcher < BaseMatcher
|
|
303
|
+
def initialize(predicate, description, *args, **kwargs, &block)
|
|
304
|
+
super(nil)
|
|
305
|
+
@predicate = predicate
|
|
306
|
+
@description = description
|
|
307
|
+
@args = args
|
|
308
|
+
@kwargs = kwargs
|
|
309
|
+
@block = block
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
def matches?(actual)
|
|
313
|
+
@actual = actual
|
|
314
|
+
unless @actual.respond_to?(@predicate)
|
|
315
|
+
raise NoMethodError, "undefined method '#{@predicate}' for #{@actual.inspect}"
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
!!@actual.public_send(@predicate, *@args, **@kwargs, &@block)
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
def failure_message
|
|
322
|
+
"Expected #{@actual.inspect} to #{@description}#{args_description}"
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
def failure_message_when_negated
|
|
326
|
+
"Expected #{@actual.inspect} not to #{@description}#{args_description}"
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
private
|
|
330
|
+
|
|
331
|
+
def args_description
|
|
332
|
+
return "" if @args.empty? && @kwargs.empty?
|
|
333
|
+
|
|
334
|
+
parts = @args.map(&:inspect) + @kwargs.map { |k, v| "#{k}: #{v.inspect}" }
|
|
335
|
+
" with #{parts.join(", ")}"
|
|
336
|
+
end
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
class HaveHttpStatusMatcher < BaseMatcher
|
|
340
|
+
STATUS_GROUPS = {
|
|
341
|
+
success: 200..299,
|
|
342
|
+
successful: 200..299,
|
|
343
|
+
redirect: 300..399,
|
|
344
|
+
error: 500..599,
|
|
345
|
+
server_error: 500..599,
|
|
346
|
+
client_error: 400..499,
|
|
347
|
+
missing: [404]
|
|
348
|
+
}.freeze
|
|
349
|
+
|
|
350
|
+
FALLBACK_CODES = {
|
|
351
|
+
ok: 200, created: 201, accepted: 202, no_content: 204,
|
|
352
|
+
moved_permanently: 301, found: 302, see_other: 303, not_modified: 304,
|
|
353
|
+
bad_request: 400, unauthorized: 401, forbidden: 403, not_found: 404,
|
|
354
|
+
method_not_allowed: 405, not_acceptable: 406, conflict: 409, gone: 410,
|
|
355
|
+
unprocessable_entity: 422, unprocessable_content: 422, too_many_requests: 429,
|
|
356
|
+
internal_server_error: 500, not_implemented: 501, bad_gateway: 502,
|
|
357
|
+
service_unavailable: 503
|
|
358
|
+
}.freeze
|
|
359
|
+
|
|
360
|
+
def initialize(expected)
|
|
361
|
+
super(expected)
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
def matches?(actual)
|
|
365
|
+
@actual = actual
|
|
366
|
+
@actual_status = actual.respond_to?(:status) ? actual.status : actual
|
|
367
|
+
expected_codes.include?(@actual_status)
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
def failure_message
|
|
371
|
+
"Expected response to have HTTP status #{@expected.inspect}, got #{@actual_status.inspect}"
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
def failure_message_when_negated
|
|
375
|
+
"Expected response not to have HTTP status #{@expected.inspect}, but it did (#{@actual_status.inspect})"
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
private
|
|
379
|
+
|
|
380
|
+
def expected_codes
|
|
381
|
+
case @expected
|
|
382
|
+
when Integer then [@expected]
|
|
383
|
+
when Range then @expected
|
|
384
|
+
when Symbol, String
|
|
385
|
+
sym = @expected.to_sym
|
|
386
|
+
group = STATUS_GROUPS[sym]
|
|
387
|
+
return group if group
|
|
388
|
+
|
|
389
|
+
if defined?(::Rack::Utils::SYMBOL_TO_STATUS_CODE)
|
|
390
|
+
code = ::Rack::Utils::SYMBOL_TO_STATUS_CODE[sym]
|
|
391
|
+
return [code] if code
|
|
392
|
+
end
|
|
393
|
+
code = FALLBACK_CODES[sym]
|
|
394
|
+
return [code] if code
|
|
395
|
+
|
|
396
|
+
raise ArgumentError, "Unknown HTTP status: #{@expected.inspect}"
|
|
397
|
+
else
|
|
398
|
+
raise ArgumentError, "Invalid HTTP status: #{@expected.inspect}"
|
|
399
|
+
end
|
|
400
|
+
end
|
|
401
|
+
end
|
|
402
|
+
|
|
403
|
+
class MatchMatcher < BaseMatcher
|
|
404
|
+
def matches?(actual)
|
|
405
|
+
@actual = actual
|
|
406
|
+
case @expected
|
|
407
|
+
when Regexp then @expected.match?(actual.to_s)
|
|
408
|
+
when String then actual.is_a?(Regexp) ? actual.match?(@expected) : actual == @expected
|
|
409
|
+
else values_match?(@expected, actual)
|
|
410
|
+
end
|
|
411
|
+
end
|
|
412
|
+
|
|
413
|
+
def failure_message
|
|
414
|
+
"Expected #{@actual.inspect} to match #{@expected.inspect}"
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
private
|
|
418
|
+
|
|
419
|
+
def values_match?(expected, actual)
|
|
420
|
+
case expected
|
|
421
|
+
when Array
|
|
422
|
+
return false unless actual.is_a?(Array) && actual.size == expected.size
|
|
423
|
+
|
|
424
|
+
expected.zip(actual).all? { |e, a| values_match?(e, a) }
|
|
425
|
+
when Hash
|
|
426
|
+
return false unless actual.is_a?(Hash) && actual.size == expected.size
|
|
427
|
+
|
|
428
|
+
expected.all? { |k, v| actual.key?(k) && values_match?(v, actual[k]) }
|
|
429
|
+
when Regexp
|
|
430
|
+
expected.match?(actual.to_s)
|
|
431
|
+
else
|
|
432
|
+
expected === actual || expected == actual
|
|
433
|
+
end
|
|
434
|
+
end
|
|
435
|
+
end
|
|
436
|
+
|
|
437
|
+
class MatchArrayMatcher < BaseMatcher
|
|
438
|
+
def matches?(actual)
|
|
439
|
+
@actual = actual
|
|
440
|
+
return false unless actual.respond_to?(:to_a)
|
|
441
|
+
|
|
442
|
+
remaining = actual.to_a.dup
|
|
443
|
+
@expected.all? do |item|
|
|
444
|
+
idx = remaining.index { |el| item == el || item === el }
|
|
445
|
+
idx ? remaining.delete_at(idx) && true : false
|
|
446
|
+
end && remaining.empty?
|
|
447
|
+
end
|
|
448
|
+
|
|
449
|
+
def failure_message
|
|
450
|
+
"Expected #{@actual.inspect} to contain exactly #{@expected.inspect} (in any order)"
|
|
451
|
+
end
|
|
452
|
+
end
|
|
453
|
+
|
|
454
|
+
class HaveAttributesMatcher < BaseMatcher
|
|
455
|
+
def matches?(actual)
|
|
456
|
+
@actual = actual
|
|
457
|
+
@mismatches = {}
|
|
458
|
+
@expected.each do |attr, value|
|
|
459
|
+
unless actual.respond_to?(attr)
|
|
460
|
+
@mismatches[attr] = :no_method
|
|
461
|
+
next
|
|
462
|
+
end
|
|
463
|
+
actual_value = actual.public_send(attr)
|
|
464
|
+
@mismatches[attr] = actual_value unless value == actual_value || value === actual_value
|
|
465
|
+
end
|
|
466
|
+
@mismatches.empty?
|
|
467
|
+
end
|
|
468
|
+
|
|
469
|
+
def failure_message
|
|
470
|
+
details = @mismatches.map do |attr, val|
|
|
471
|
+
if val == :no_method
|
|
472
|
+
"#{attr}: (does not respond)"
|
|
473
|
+
else
|
|
474
|
+
"#{attr}: expected #{@expected[attr].inspect}, got #{val.inspect}"
|
|
475
|
+
end
|
|
476
|
+
end
|
|
477
|
+
"Expected #{@actual.inspect} to have attributes #{@expected.inspect}:\n #{details.join("\n ")}"
|
|
478
|
+
end
|
|
479
|
+
end
|
|
480
|
+
|
|
481
|
+
class AllMatcher < BaseMatcher
|
|
482
|
+
def initialize(inner)
|
|
483
|
+
super()
|
|
484
|
+
@inner = inner
|
|
485
|
+
end
|
|
486
|
+
|
|
487
|
+
def matches?(actual)
|
|
488
|
+
@actual = actual
|
|
489
|
+
@failed_index = nil
|
|
490
|
+
actual.each_with_index do |item, idx|
|
|
491
|
+
next if @inner.matches?(item)
|
|
492
|
+
|
|
493
|
+
@failed_index = idx
|
|
494
|
+
@failed_message = @inner.failure_message
|
|
495
|
+
return false
|
|
496
|
+
end
|
|
497
|
+
true
|
|
498
|
+
end
|
|
499
|
+
|
|
500
|
+
def failure_message
|
|
501
|
+
"Expected all elements to match, but element at index #{@failed_index} failed: #{@failed_message}"
|
|
502
|
+
end
|
|
503
|
+
end
|
|
504
|
+
|
|
505
|
+
class SatisfyMatcher < BaseMatcher
|
|
506
|
+
def initialize(description = nil, &block)
|
|
507
|
+
super(nil)
|
|
508
|
+
@description = description
|
|
509
|
+
@block = block
|
|
510
|
+
end
|
|
511
|
+
|
|
512
|
+
def matches?(actual)
|
|
513
|
+
@actual = actual
|
|
514
|
+
!!@block.call(actual)
|
|
515
|
+
end
|
|
516
|
+
|
|
517
|
+
def failure_message
|
|
518
|
+
"Expected #{@actual.inspect} to satisfy #{@description || "the given block"}"
|
|
519
|
+
end
|
|
520
|
+
end
|
|
521
|
+
|
|
522
|
+
class BeWithinMatcher < BaseMatcher
|
|
523
|
+
def initialize(delta)
|
|
524
|
+
super(delta)
|
|
525
|
+
@delta = delta
|
|
526
|
+
end
|
|
527
|
+
|
|
528
|
+
def of(value)
|
|
529
|
+
@center = value
|
|
530
|
+
self
|
|
531
|
+
end
|
|
532
|
+
|
|
533
|
+
def percent_of(value)
|
|
534
|
+
@center = value
|
|
535
|
+
@delta = value.abs * @expected / 100.0
|
|
536
|
+
self
|
|
537
|
+
end
|
|
538
|
+
|
|
539
|
+
def matches?(actual)
|
|
540
|
+
raise ArgumentError, "be_within requires .of(value)" unless defined?(@center)
|
|
541
|
+
|
|
542
|
+
@actual = actual
|
|
543
|
+
(actual - @center).abs <= @delta
|
|
544
|
+
end
|
|
545
|
+
|
|
546
|
+
def failure_message
|
|
547
|
+
"Expected #{@actual.inspect} to be within #{@delta} of #{@center}"
|
|
548
|
+
end
|
|
549
|
+
end
|
|
550
|
+
|
|
551
|
+
class StartWithMatcher < BaseMatcher
|
|
552
|
+
def matches?(actual)
|
|
553
|
+
@actual = actual
|
|
554
|
+
if actual.respond_to?(:start_with?)
|
|
555
|
+
actual.start_with?(@expected)
|
|
556
|
+
elsif actual.is_a?(Array)
|
|
557
|
+
actual.first(Array(@expected).size) == Array(@expected)
|
|
558
|
+
else
|
|
559
|
+
false
|
|
560
|
+
end
|
|
561
|
+
end
|
|
562
|
+
|
|
563
|
+
def failure_message
|
|
564
|
+
"Expected #{@actual.inspect} to start with #{@expected.inspect}"
|
|
565
|
+
end
|
|
566
|
+
end
|
|
567
|
+
|
|
568
|
+
class EndWithMatcher < BaseMatcher
|
|
569
|
+
def matches?(actual)
|
|
570
|
+
@actual = actual
|
|
571
|
+
if actual.respond_to?(:end_with?)
|
|
572
|
+
actual.end_with?(@expected)
|
|
573
|
+
elsif actual.is_a?(Array)
|
|
574
|
+
actual.last(Array(@expected).size) == Array(@expected)
|
|
575
|
+
else
|
|
576
|
+
false
|
|
577
|
+
end
|
|
578
|
+
end
|
|
579
|
+
|
|
580
|
+
def failure_message
|
|
581
|
+
"Expected #{@actual.inspect} to end with #{@expected.inspect}"
|
|
582
|
+
end
|
|
583
|
+
end
|
|
584
|
+
|
|
585
|
+
class OutputMatcher < BaseMatcher
|
|
586
|
+
def initialize(expected = nil)
|
|
587
|
+
super
|
|
588
|
+
@stream = :stdout
|
|
589
|
+
end
|
|
590
|
+
|
|
591
|
+
def to_stdout
|
|
592
|
+
@stream = :stdout
|
|
593
|
+
self
|
|
594
|
+
end
|
|
595
|
+
|
|
596
|
+
def to_stderr
|
|
597
|
+
@stream = :stderr
|
|
598
|
+
self
|
|
599
|
+
end
|
|
600
|
+
|
|
601
|
+
def matches?(block)
|
|
602
|
+
raise ArgumentError, "output matcher requires a block" unless block.respond_to?(:call)
|
|
603
|
+
|
|
604
|
+
@captured = capture(block)
|
|
605
|
+
if @expected.nil?
|
|
606
|
+
!@captured.empty?
|
|
607
|
+
elsif @expected.is_a?(Regexp)
|
|
608
|
+
@expected.match?(@captured)
|
|
609
|
+
else
|
|
610
|
+
@captured.include?(@expected)
|
|
611
|
+
end
|
|
612
|
+
end
|
|
613
|
+
|
|
614
|
+
def failure_message
|
|
615
|
+
"Expected block to output #{@expected ? @expected.inspect : "something"} to #{@stream}, got #{@captured.inspect}"
|
|
616
|
+
end
|
|
617
|
+
|
|
618
|
+
private
|
|
619
|
+
|
|
620
|
+
def capture(block)
|
|
621
|
+
require "stringio"
|
|
622
|
+
captured = StringIO.new
|
|
623
|
+
if @stream == :stdout
|
|
624
|
+
original = $stdout
|
|
625
|
+
$stdout = captured
|
|
626
|
+
begin
|
|
627
|
+
block.call
|
|
628
|
+
ensure
|
|
629
|
+
$stdout = original
|
|
630
|
+
end
|
|
631
|
+
else
|
|
632
|
+
original = $stderr
|
|
633
|
+
$stderr = captured
|
|
634
|
+
begin
|
|
635
|
+
block.call
|
|
636
|
+
ensure
|
|
637
|
+
$stderr = original
|
|
638
|
+
end
|
|
639
|
+
end
|
|
640
|
+
captured.string
|
|
641
|
+
end
|
|
642
|
+
end
|
|
643
|
+
|
|
644
|
+
class YieldControlMatcher < BaseMatcher
|
|
645
|
+
def matches?(block)
|
|
646
|
+
@yielded = 0
|
|
647
|
+
probe = ->(*) { @yielded += 1 }
|
|
648
|
+
block.call(probe)
|
|
649
|
+
@yielded.positive?
|
|
650
|
+
end
|
|
651
|
+
|
|
652
|
+
def failure_message
|
|
653
|
+
"Expected block to yield control, but it did not"
|
|
654
|
+
end
|
|
655
|
+
|
|
656
|
+
def failure_message_when_negated
|
|
657
|
+
"Expected block not to yield control, but it yielded #{@yielded} time(s)"
|
|
658
|
+
end
|
|
659
|
+
end
|
|
660
|
+
|
|
661
|
+
class YieldWithArgsMatcher < BaseMatcher
|
|
662
|
+
def initialize(*expected_args)
|
|
663
|
+
super(expected_args)
|
|
664
|
+
@expected_args = expected_args
|
|
665
|
+
end
|
|
666
|
+
|
|
667
|
+
def matches?(block)
|
|
668
|
+
@yielded_args = nil
|
|
669
|
+
probe = ->(*args) { @yielded_args = args }
|
|
670
|
+
block.call(probe)
|
|
671
|
+
return false if @yielded_args.nil?
|
|
672
|
+
return true if @expected_args.empty?
|
|
673
|
+
return false unless @yielded_args.size == @expected_args.size
|
|
674
|
+
|
|
675
|
+
@expected_args.zip(@yielded_args).all? { |e, a| e == a || e === a }
|
|
676
|
+
end
|
|
677
|
+
|
|
678
|
+
def failure_message
|
|
679
|
+
if @yielded_args.nil?
|
|
680
|
+
"Expected block to yield with args #{@expected_args.inspect}, but it did not yield"
|
|
681
|
+
else
|
|
682
|
+
"Expected block to yield with args #{@expected_args.inspect}, got #{@yielded_args.inspect}"
|
|
683
|
+
end
|
|
684
|
+
end
|
|
685
|
+
end
|
|
686
|
+
|
|
687
|
+
class YieldSuccessiveArgsMatcher < BaseMatcher
|
|
688
|
+
def initialize(*expected_args)
|
|
689
|
+
super(expected_args)
|
|
690
|
+
@expected_args = expected_args
|
|
691
|
+
end
|
|
692
|
+
|
|
693
|
+
def matches?(block)
|
|
694
|
+
@yielded = []
|
|
695
|
+
probe = ->(*args) { @yielded << (args.size == 1 ? args.first : args) }
|
|
696
|
+
block.call(probe)
|
|
697
|
+
return false unless @yielded.size == @expected_args.size
|
|
698
|
+
|
|
699
|
+
@expected_args.zip(@yielded).all? { |e, a| e == a || e === a }
|
|
700
|
+
end
|
|
701
|
+
|
|
702
|
+
def failure_message
|
|
703
|
+
"Expected block to yield successive args #{@expected_args.inspect}, got #{@yielded.inspect}"
|
|
704
|
+
end
|
|
705
|
+
end
|
|
706
|
+
|
|
707
|
+
class CustomMatcher < BaseMatcher
|
|
708
|
+
def initialize(name, expected_args, &definition_block)
|
|
709
|
+
super(expected_args)
|
|
710
|
+
@name = name
|
|
711
|
+
@expected_args = expected_args
|
|
712
|
+
@definition_block = definition_block
|
|
713
|
+
@match_proc = nil
|
|
714
|
+
@failure_message_proc = nil
|
|
715
|
+
@negated_failure_message_proc = nil
|
|
716
|
+
@description_proc = nil
|
|
717
|
+
@chains = {}
|
|
718
|
+
instance_exec(*expected_args, &definition_block) if definition_block
|
|
719
|
+
end
|
|
720
|
+
|
|
721
|
+
def match(&block)
|
|
722
|
+
@match_proc = block
|
|
723
|
+
end
|
|
724
|
+
|
|
725
|
+
# DSL form (with block) stores the message proc; query form (no
|
|
726
|
+
# block) returns the rendered message.
|
|
727
|
+
def failure_message(&block)
|
|
728
|
+
if block
|
|
729
|
+
@failure_message_proc = block
|
|
730
|
+
return
|
|
731
|
+
end
|
|
732
|
+
if @failure_message_proc
|
|
733
|
+
instance_exec(@actual, &@failure_message_proc)
|
|
734
|
+
else
|
|
735
|
+
"Expected #{@actual.inspect} to #{description}"
|
|
736
|
+
end
|
|
737
|
+
end
|
|
738
|
+
|
|
739
|
+
def failure_message_when_negated(&block)
|
|
740
|
+
if block
|
|
741
|
+
@negated_failure_message_proc = block
|
|
742
|
+
return
|
|
743
|
+
end
|
|
744
|
+
if @negated_failure_message_proc
|
|
745
|
+
instance_exec(@actual, &@negated_failure_message_proc)
|
|
746
|
+
else
|
|
747
|
+
"Expected #{@actual.inspect} not to #{description}"
|
|
748
|
+
end
|
|
749
|
+
end
|
|
750
|
+
|
|
751
|
+
def description(&block)
|
|
752
|
+
if block
|
|
753
|
+
@description_proc = block
|
|
754
|
+
return
|
|
755
|
+
end
|
|
756
|
+
@description_proc ? instance_exec(&@description_proc) : @name.to_s.tr("_", " ")
|
|
757
|
+
end
|
|
758
|
+
|
|
759
|
+
def chain(name, &block)
|
|
760
|
+
matcher = self
|
|
761
|
+
define_singleton_method(name) do |*args|
|
|
762
|
+
matcher.instance_variable_get(:@chains)[name] = args
|
|
763
|
+
matcher.instance_exec(*args, &block) if block
|
|
764
|
+
matcher
|
|
765
|
+
end
|
|
766
|
+
end
|
|
767
|
+
|
|
768
|
+
def matches?(actual)
|
|
769
|
+
@actual = actual
|
|
770
|
+
@match_proc ? instance_exec(actual, &@match_proc) : true
|
|
771
|
+
end
|
|
772
|
+
end
|
|
773
|
+
|
|
222
774
|
def eq(expected)
|
|
223
775
|
EqMatcher.new(expected)
|
|
224
776
|
end
|
|
@@ -238,6 +790,19 @@ module Crspec
|
|
|
238
790
|
def be_falsy
|
|
239
791
|
BeFalsyMatcher.new
|
|
240
792
|
end
|
|
793
|
+
alias be_falsey be_falsy
|
|
794
|
+
|
|
795
|
+
def be_a(klass)
|
|
796
|
+
BeAKindOfMatcher.new(klass)
|
|
797
|
+
end
|
|
798
|
+
alias be_an be_a
|
|
799
|
+
alias be_a_kind_of be_a
|
|
800
|
+
alias be_kind_of be_a
|
|
801
|
+
|
|
802
|
+
def be_an_instance_of(klass)
|
|
803
|
+
BeAnInstanceOfMatcher.new(klass)
|
|
804
|
+
end
|
|
805
|
+
alias be_instance_of be_an_instance_of
|
|
241
806
|
|
|
242
807
|
def include(*expected)
|
|
243
808
|
IncludeMatcher.new(expected.size == 1 ? expected.first : expected)
|
|
@@ -254,5 +819,82 @@ module Crspec
|
|
|
254
819
|
def change(receiver = nil, message = nil, &block)
|
|
255
820
|
ChangeMatcher.new(receiver, message, &block)
|
|
256
821
|
end
|
|
822
|
+
|
|
823
|
+
def have_http_status(expected)
|
|
824
|
+
HaveHttpStatusMatcher.new(expected)
|
|
825
|
+
end
|
|
826
|
+
|
|
827
|
+
def match(expected)
|
|
828
|
+
MatchMatcher.new(expected)
|
|
829
|
+
end
|
|
830
|
+
|
|
831
|
+
def match_array(expected)
|
|
832
|
+
MatchArrayMatcher.new(expected.to_a)
|
|
833
|
+
end
|
|
834
|
+
|
|
835
|
+
def contain_exactly(*items)
|
|
836
|
+
MatchArrayMatcher.new(items)
|
|
837
|
+
end
|
|
838
|
+
|
|
839
|
+
def have_attributes(expected)
|
|
840
|
+
HaveAttributesMatcher.new(expected)
|
|
841
|
+
end
|
|
842
|
+
|
|
843
|
+
def all(matcher)
|
|
844
|
+
AllMatcher.new(matcher)
|
|
845
|
+
end
|
|
846
|
+
|
|
847
|
+
def satisfy(description = nil, &block)
|
|
848
|
+
SatisfyMatcher.new(description, &block)
|
|
849
|
+
end
|
|
850
|
+
|
|
851
|
+
def be_within(delta)
|
|
852
|
+
BeWithinMatcher.new(delta)
|
|
853
|
+
end
|
|
854
|
+
|
|
855
|
+
def start_with(expected)
|
|
856
|
+
StartWithMatcher.new(expected)
|
|
857
|
+
end
|
|
858
|
+
|
|
859
|
+
def end_with(expected)
|
|
860
|
+
EndWithMatcher.new(expected)
|
|
861
|
+
end
|
|
862
|
+
|
|
863
|
+
def output(expected = nil)
|
|
864
|
+
OutputMatcher.new(expected)
|
|
865
|
+
end
|
|
866
|
+
|
|
867
|
+
def yield_control
|
|
868
|
+
YieldControlMatcher.new
|
|
869
|
+
end
|
|
870
|
+
|
|
871
|
+
def yield_with_args(*args)
|
|
872
|
+
YieldWithArgsMatcher.new(*args)
|
|
873
|
+
end
|
|
874
|
+
|
|
875
|
+
def yield_successive_args(*args)
|
|
876
|
+
YieldSuccessiveArgsMatcher.new(*args)
|
|
877
|
+
end
|
|
878
|
+
|
|
879
|
+
def self.define(name, &block)
|
|
880
|
+
define_method(name) do |*args|
|
|
881
|
+
CustomMatcher.new(name, args, &block)
|
|
882
|
+
end
|
|
883
|
+
end
|
|
884
|
+
|
|
885
|
+
def method_missing(name, *args, **kwargs, &block)
|
|
886
|
+
case name
|
|
887
|
+
when /\Abe_(.+)\z/
|
|
888
|
+
PredicateMatcher.new(:"#{Regexp.last_match(1)}?", "be #{Regexp.last_match(1).tr("_", " ")}", *args, **kwargs, &block)
|
|
889
|
+
when /\Ahave_(.+)\z/
|
|
890
|
+
PredicateMatcher.new(:"has_#{Regexp.last_match(1)}?", "have #{Regexp.last_match(1).tr("_", " ")}", *args, **kwargs, &block)
|
|
891
|
+
else
|
|
892
|
+
super
|
|
893
|
+
end
|
|
894
|
+
end
|
|
895
|
+
|
|
896
|
+
def respond_to_missing?(name, include_private = false)
|
|
897
|
+
name.to_s.match?(/\A(be|have)_.+\z/) || super
|
|
898
|
+
end
|
|
257
899
|
end
|
|
258
900
|
end
|