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}