← Back to index

lib/BDD/Behave/Mock/Stub.rakumod

94 / 106 lines covered (88.7%)

1unit module BDD::Behave::Mock::Stub;23use BDD::Behave::Mock::ArgMatcher;4use BDD::Behave::Mock::Double;56class StubRegistry is export {7  my @active;89  method register($stub) {10    @active.push($stub);11    $stub;12  }1314  method find-existing(Mu $target is raw, Str:D $method-name) {15    for @active -> $s {16      next unless $s.method-name eq $method-name;17      next unless $s.target === $target;18      return $s;19    }20    Nil;21  }2223  method any-for(Mu $target is raw --> Bool) {24    for @active -> $s {25      return True if $s.target === $target;26    }27    False;28  }2930  method remove($stub) {31    my $idx = @active.first({ $_ === $stub }, :k);32    return unless $idx.defined;33    @active.splice($idx, 1);34    try {35      $stub.uninstall;36      CATCH { default { warn "Failed to uninstall stub: {.message}" } }37    }38  }3940  method clear-all {41    self.clear-since(0);42  }4344  method clear-since(Int $snapshot) {45    while @active.elems > $snapshot {46      my $stub = @active.pop;47      try {48        $stub.uninstall;49        CATCH { default { warn "Failed to uninstall stub: {.message}" } }50      }51    }52  }5354  method active-count(--> Int) { +@active }55}5657class Stub is export {58  has Mu      $.target;59  has Bool    $.target-defined;60  has Str     $.method-name is required;61  has Mu      $!routine;62  has         $!handle;63  has Bool    $!installed = False;64  has Str     $.mode = 'return';65  has Mu      $.return-value = Any;66  has Mu      $.exception;67  has Mu      $.callable-stub;68  has Bool    $.is-double = False;69  has Bool    $!stub-set = False;70  has Mu      $.previous-double-stub;71  has Bool    $.had-previous-double-stub = False;72  has Call    @!calls;7374  method calls() { @!calls.List }75  method calls-of(Str:D $m) { @!calls.grep({ .method eq $m }).List }76  method received(Str:D $m --> Bool) { @!calls.first({ .method eq $m }).defined }77  method call-count(Str:D $m --> Int) { +@!calls.grep({ .method eq $m }) }7879  method record-call(|c) {80    my $caller = user-callframe();81    @!calls.push: Call.new(82      :method($!method-name),83      :args(c.list),84      :named(c.hash),85      :file($caller.defined ?? $caller.file.IO !! IO::Path),86      :line($caller.defined ?? $caller.line.Int !! 0),87    );88  }8990  submethod BUILD(Mu :$target is raw, :$!method-name) {91    $!target := $target;92    $!target-defined = $target.defined && $target.DEFINITE;93    $!is-double = $target.WHAT === Double;94  }9596  method and-return(Mu $value is raw) {97    $!return-value := $value;98    $!mode = 'return';99    $!stub-set = True;100    self.refresh-double-stub if $!is-double;101    self;102  }103104  method and-raise(Mu $exception is raw) {105    $!exception := $exception;106    $!mode = 'raise';107    $!stub-set = True;108    self.refresh-double-stub if $!is-double;109    self;110  }111112  method and-call-original {113    if $!is-double {114      die "and-call-original is not supported on a Double — there is no original method";115    }116    $!mode = 'call-original';117    $!stub-set = True;118    self;119  }120121  method and-do(&callable) {122    $!callable-stub = &callable;123    $!mode = 'callable';124    $!stub-set = True;125    self.refresh-double-stub if $!is-double;126    self;127  }128129  method install {130    return if $!installed;131132    if $!is-double {133      $!had-previous-double-stub = $!target.stubs{$!method-name}:exists;134      $!previous-double-stub = $!target.stubs{$!method-name} if $!had-previous-double-stub;135      self.refresh-double-stub;136      $!installed = True;137      return self;138    }139140    my $owner = $!target-defined ?? $!target.WHAT !! $!target;141    my &meth = $owner.^find_method($!method-name);142143    unless &meth.defined {144      die "allow(): cannot stub '{$!method-name}' on {$owner.^name}; "145          ~ "no such method";146    }147148    $!routine := &meth;149150    my $stub = self;151    $!handle = &meth.wrap(method (|c) {152      if $stub.target-defined && self !=== $stub.target {153        return callsame;154      }155156      $stub.record-call(|c);157158      given $stub.mode {159        when 'raise'         { die $stub.exception }160        when 'callable'      { return $stub.callable-stub.(|c) }161        when 'call-original' { return callsame }162        default              { return $stub.return-value }163      }164    });165    $!installed = True;166    self;167  }168169  method refresh-double-stub {170    return unless $!is-double;171    my %stubs := $!target.raw-stubs;172    given $!mode {173      when 'raise' {174        my $ex := $!exception;175        %stubs{$!method-name} = sub (|c) { die $ex };176      }177      when 'callable' {178        %stubs{$!method-name} = $!callable-stub;179      }180      default {181        %stubs{$!method-name} = $!return-value;182      }183    }184  }185186  method uninstall {187    return unless $!installed;188189    if $!is-double {190      my %stubs := $!target.raw-stubs;191      if $!had-previous-double-stub {192        %stubs{$!method-name} = $!previous-double-stub;193      } else {194        %stubs{$!method-name}:delete;195      }196      $!installed = False;197      return self;198    }199200    if $!routine.defined && $!handle.defined {201      try {202        $!routine.unwrap($!handle);203        CATCH { default { warn "Stub unwrap failed: {.message}" } }204      }205    }206    $!installed = False;207    self;208  }209}