102 / 108 lines covered (94.4%)
1unit module BDD::Behave::Mock::HaveReceived;23use BDD::Behave::Failure;4use BDD::Behave::Failures;5use BDD::Behave::Mock::ArgMatcher;6use BDD::Behave::Mock::Double;7use BDD::Behave::Mock::Stub;89class HaveReceivedExpectation is export {10 has Mu $.target;11 has Str $.method-name is required;12 has Bool $.negated = False;13 has Str $.file is required;14 has Int $.line is required;15 has Bool $!has-with-filter = False;16 has @!with-args;17 has %!with-named;18 has Str $!count-kind = 'present';19 has Int $!count-n = 0;20 has $!recorded-failure;2122 submethod BUILD(Mu :$target is raw, :$!method-name, :$!negated, :$!file, :$!line) {23 $!target := $target;24 }2526 method calls() {27 if $!target.WHAT === Double {28 return $!target.calls-of($!method-name);29 }3031 my $stub = StubRegistry.find-existing($!target, $!method-name);32 return $stub.defined ?? $stub.calls-of($!method-name) !! ();33 }3435 method has-recorder(--> Bool) {36 return True if $!target.WHAT === Double;37 StubRegistry.find-existing($!target, $!method-name).defined;38 }3940 method match-call(Call $c --> Bool) {41 return True unless $!has-with-filter;42 return False unless @!with-args.elems == $c.args.elems;43 for @!with-args.kv -> $i, $expected {44 my $actual = $c.args[$i];45 if $expected ~~ ArgMatcher {46 return False unless $expected.matches($actual);47 } else {48 return False unless $actual ~~ $expected;49 }50 }51 for %!with-named.kv -> $k, $expected {52 return False unless $c.named{$k}:exists;53 my $actual = $c.named{$k};54 if $expected ~~ ArgMatcher {55 return False unless $expected.matches($actual);56 } else {57 return False unless $actual ~~ $expected;58 }59 }60 True;61 }6263 method matching-calls() {64 self.calls.grep({ self.match-call($_) }).List;65 }6667 method count-met(Int $count --> Bool) {68 given $!count-kind {69 when 'present' { return $count > 0 }70 when 'exactly' { return $count == $!count-n }71 when 'at-least' { return $count >= $!count-n }72 when 'at-most' { return $count <= $!count-n }73 }74 False;75 }7677 method count-clause(--> Str) {78 given $!count-kind {79 when 'present' { 'at least once' }80 when 'exactly' {81 $!count-n == 1 ?? 'exactly once'82 !! $!count-n == 2 ?? 'exactly twice'83 !! "exactly {$!count-n} times"84 }85 when 'at-least' {86 $!count-n == 1 ?? 'at least once'87 !! "at least {$!count-n} times"88 }89 when 'at-most' {90 $!count-n == 1 ?? 'at most once'91 !! "at most {$!count-n} times"92 }93 default { 'at least once' }94 }95 }9697 method args-clause(--> Str) {98 return '' unless $!has-with-filter;99 my @parts;100 for @!with-args -> $a {101 @parts.push: $a ~~ ArgMatcher ?? $a.describe !! $a.raku;102 }103 for %!with-named.kv -> $k, $v {104 my $val = $v ~~ ArgMatcher ?? $v.describe !! $v.raku;105 @parts.push: ":$k\($val)";106 }107 " with ({@parts.join(', ')})";108 }109110 method target-description(--> Str) {111 if $!target.WHAT === Double {112 my $name = $!target.double-name;113 return "double($name.raku())#{$!method-name}";114 }115 my $owner = ($!target.defined && $!target.DEFINITE) ?? $!target.WHAT !! $!target;116 "{$owner.^name}#{$!method-name}";117 }118119 method validate(--> Nil) {120 unless self.has-recorder {121 my $msg = "expect({self.target-description}).to.have-received(...): "122 ~ "no stub installed; call `allow($!target.^name())"123 ~ ".to.receive('{$!method-name}')` first or use `spy(\$obj)`";124 self.replace-failure($msg);125 return;126 }127128 my @matches = self.matching-calls;129 my $count = @matches.elems;130 my $met = self.count-met($count);131 $met = !$met if $!negated;132133 if $met {134 self.clear-failure;135 return;136 }137138 my $verb = $!negated ?? 'not to have been called' !! 'to have been called';139 my $expected = self.count-clause ~ self.args-clause;140 my $actual-clause = self.actual-clause($count);141142 my $msg = "expected {self.target-description} $verb $expected\n got $actual-clause";143 self.replace-failure($msg);144 }145146 method actual-clause(Int $observed --> Str) {147 my $total = self.calls.elems;148 if $!has-with-filter {149 return "$observed matching call{$observed == 1 ?? '' !! 's'} (out of $total total)";150 }151 "$total call{$total == 1 ?? '' !! 's'}";152 }153154 method replace-failure(Str $msg --> Nil) {155 self.clear-failure;156 my $f = Failure.new(157 :$!file, :$!line, :message($msg), :negated($!negated),158 );159 Failures.list.push($f);160 $!recorded-failure = $f;161 }162163 method clear-failure(--> Nil) {164 return unless $!recorded-failure.defined;165 my $list = Failures.list;166 my $idx = $list.first({ $_ === $!recorded-failure }, :k);167 $list.splice($idx, 1) if $idx.defined;168 $!recorded-failure = Nil;169 }170171 method with(*@a, *%n) {172 @!with-args = @a;173 %!with-named = %n;174 $!has-with-filter = True;175 self.validate;176 self;177 }178179 method times(Int:D $n) {180 $!count-kind = 'exactly';181 $!count-n = $n;182 self.validate;183 self;184 }185186 method exactly(Int:D $n) { self.times($n) }187 method once { self.times(1) }188 method twice { self.times(2) }189 method thrice { self.times(3) }190191 method at-least(Int:D $n) {192 $!count-kind = 'at-least';193 $!count-n = $n;194 self.validate;195 self;196 }197198 method at-most(Int:D $n) {199 $!count-kind = 'at-most';200 $!count-n = $n;201 self.validate;202 self;203 }204}