← Back to index

lib/BDD/Behave/Mock/HaveReceived.rakumod

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}