| File: | blib/lib/Test/Mockingbird.pm |
| Coverage: | 99.5% |
| line | stmt | bran | cond | sub | time | code |
|---|---|---|---|---|---|---|
| 1 | package Test::Mockingbird; | |||||
| 2 | ||||||
| 3 | 26 26 26 | 1678772 36 337 | use strict; | |||
| 4 | 26 26 26 | 38 16 454 | use warnings; | |||
| 5 | 26 26 | 218 38 | use 5.016003; | |||
| 6 | ||||||
| 7 | # --------------------------------------------------------------------------- | |||||
| 8 | # Roadmap -- future work | |||||
| 9 | # --------------------------------------------------------------------------- | |||||
| 10 | # | |||||
| 11 | # TODO: Strict mode (use Test::Mockingbird ':strict', or a per-call option): | |||||
| 12 | # croak when the target does not exist in the package or its parents. | |||||
| 13 | # Today a misspelled name (Pkg::fetch typed wrongly) creates a new sub and | |||||
| 14 | # the test passes against the real method (documented in LIMITATIONS). | |||||
| 15 | # | |||||
| 16 | # TODO: Argument-dispatching mocks, e.g. | |||||
| 17 | # mock_when 'Pkg::m' => [qr/foo/] => 'a', [1, 2] => 'b', | |||||
| 18 | # default => sub { ... }; | |||||
| 19 | # mock_sequence/mock_exception exist; dispatch on arguments does not. | |||||
| 20 | # | |||||
| 21 | # TODO: Assertion methods on spies: $spy->called_ok, ->called_with(...), | |||||
| 22 | # ->call_count. Users currently count the list from $spy->() by hand; | |||||
| 23 | # DeepMock has these checks, but only inside its own DSL. Needs spy() | |||||
| 24 | # to return a blessed callable object so existing $spy->() callers keep | |||||
| 25 | # working. | |||||
| 26 | # | |||||
| 27 | # TODO: Automatic restore_all() at the end of each subtest via a Test2 hook | |||||
| 28 | # (opt-in), so a forgotten unmock cannot leak into later subtests -- | |||||
| 29 | # the failure mode that exposed GH#14. | |||||
| 30 | # | |||||
| 31 | # TODO: $ENV{TEST_MOCKINGBIRD_DEBUG} tracing flag that emits one line to | |||||
| 32 | # STDERR on each mock/unmock/restore_all operation (name, type, caller | |||||
| 33 | # location). diagnose_mocks() covers post-hoc inspection; this would add | |||||
| 34 | # real-time per-operation tracing for troubleshooting complex stacking | |||||
| 35 | # scenarios. Inspired by Function::Override's PERL_FUNCTION_OVERRIDE_DEBUG. | |||||
| 36 | # | |||||
| 37 | # TODO: Bridge TimeTravel and mock_core('time') (e.g. freeze_time(..., core | |||||
| 38 | # => 1)) so code compiled after the freeze sees the frozen clock through | |||||
| 39 | # the builtin, not only through TimeTravel::now(). | |||||
| 40 | # | |||||
| 41 | # TODO: Per-object mocks: mock one instance without touching its class, by | |||||
| 42 | # blessing it into a generated singleton subclass that restore puts | |||||
| 43 | # back. t/object.t currently exercises class-wide mocks only. | |||||
| 44 | # | |||||
| 45 | # TODO: AUTOLOAD-aware call-through: when _inherited_method() finds nothing, | |||||
| 46 | # fall back to the first AUTOLOAD in the MRO (setting $AUTOLOAD) instead | |||||
| 47 | # of croaking "Undefined subroutine" (documented in LIMITATIONS). | |||||
| 48 | # | |||||
| 49 | # TODO: Tests and documentation for mocking methods that carry Moo/Moose | |||||
| 50 | # method modifiers or come from roles. | |||||
| 51 | # | |||||
| 52 | # TODO: mock_core() call-through for builtins whose prototypes take | |||||
| 53 | # references (tie, pos, dbmopen, dbmclose): their $call_builtin croaks. | |||||
| 54 | # Could be done by generating a per-call-site delegator that | |||||
| 55 | # dereferences the argument. | |||||
| 56 | # | |||||
| 57 | # --------------------------------------------------------------------------- | |||||
| 58 | # Technical debt | |||||
| 59 | # --------------------------------------------------------------------------- | |||||
| 60 | # | |||||
| 61 | # TODO: Consolidate target parsing. mock(), unmock(), before(), after() and | |||||
| 62 | # around() each repeat the shorthand/longhand regex instead of calling | |||||
| 63 | # _parse_target(), and three different discriminators are in use | |||||
| 64 | # (!defined $arg3, !defined $arg2, @_ == 2 in inject()). The duplicated | |||||
| 65 | # conditions also show up as half-covered in Devel::Cover. | |||||
| 66 | # | |||||
| 67 | # TODO: Move %mocked, %mock_meta and @call_log into a registry object. This | |||||
| 68 | # would allow independent mock sessions, fork-safe state, and a cleaner | |||||
| 69 | # extension point for DeepMock/Async than the 'local $TYPE' side channel. | |||||
| 70 | # | |||||
| 71 | # TODO: Let mock_scoped() guards own spies (and before/after/around layers) | |||||
| 72 | # so mixing them no longer needs restore_all(). | |||||
| 73 | # | |||||
| 74 | # TODO: _is_core_overridable() only checks that prototype("CORE::$name") | |||||
| 75 | # does not die, so non-overridable keywords whose prototype is undef | |||||
| 76 | # (defined, exists, delete, my, ...) are accepted and mock_core() | |||||
| 77 | # installs an override Perl never consults. Use an explicit list of | |||||
| 78 | # non-overridable keywords. | |||||
| 79 | # | |||||
| 80 | # TODO: Mutation pipeline (App::Test::Generator): stop re-emitting survivors | |||||
| 81 | # already killed by t/mutant_killers.t, stop generating new_ok() stubs | |||||
| 82 | # for this function-based module, and prune or archive old xt/ stubs | |||||
| 83 | # automatically. | |||||
| 84 | ||||||
| 85 | 26 26 26 | 60 29 576 | use Carp qw(croak carp); | |||
| 86 | 26 26 26 | 63 21 288 | use Exporter 'import'; | |||
| 87 | 26 26 26 | 49 22 155 | use Scalar::Util (); | |||
| 88 | 26 26 26 | 61 15 559 | use mro (); | |||
| 89 | ||||||
| 90 | # Internal type-name constants -- eliminate magic strings. | |||||
| 91 | # These constants are used wherever a layer type is recorded in %mock_meta. | |||||
| 92 | use constant { | |||||
| 93 | 26 | 5510 | _T_MOCK => 'mock', | |||
| 94 | _T_SPY => 'spy', | |||||
| 95 | _T_INJECT => 'inject', | |||||
| 96 | _T_MOCK_RETURN => 'mock_return', | |||||
| 97 | _T_MOCK_EXCEPT => 'mock_exception', | |||||
| 98 | _T_MOCK_SEQ => 'mock_sequence', | |||||
| 99 | _T_MOCK_ONCE => 'mock_once', | |||||
| 100 | _T_MOCK_SCOPED => 'mock_scoped', | |||||
| 101 | _T_INTERCEPT_NEW => 'intercept_new', | |||||
| 102 | _T_BEFORE => 'before', | |||||
| 103 | _T_AFTER => 'after', | |||||
| 104 | _T_AROUND => 'around', | |||||
| 105 | _T_MOCK_CORE => 'mock_core', | |||||
| 106 | 26 26 | 82 21 | }; | |||
| 107 | ||||||
| 108 | our @EXPORT = qw( | |||||
| 109 | mock | |||||
| 110 | unmock | |||||
| 111 | mock_scoped | |||||
| 112 | mock_core | |||||
| 113 | before | |||||
| 114 | after | |||||
| 115 | around | |||||
| 116 | spy | |||||
| 117 | inject | |||||
| 118 | inject_all | |||||
| 119 | intercept_new | |||||
| 120 | restore | |||||
| 121 | restore_all | |||||
| 122 | mock_return | |||||
| 123 | mock_exception | |||||
| 124 | mock_sequence | |||||
| 125 | mock_once | |||||
| 126 | diagnose_mocks | |||||
| 127 | diagnose_mocks_pretty | |||||
| 128 | assert_call_order | |||||
| 129 | clear_call_log | |||||
| 130 | ); | |||||
| 131 | ||||||
| 132 | # $TYPE is set via 'local' by sugar functions before delegating to mock() | |||||
| 133 | # or inject() so that diagnose_mocks() records the correct layer type. | |||||
| 134 | # External modules (e.g. Test::Mockingbird::Async) use the same mechanism. | |||||
| 135 | our $TYPE; | |||||
| 136 | ||||||
| 137 | # Internal mocking state -- module-level lexicals. | |||||
| 138 | my %mocked; # full_method => [ stack of saved coderefs; undef = no sub was declared ] | |||||
| 139 | my %mock_meta; # full_method => [ { type => ..., installed_at => ... }, ... ] | |||||
| 140 | my @call_log; # ordered log of every spied call | |||||
| 141 | ||||||
| 142 | =encoding utf-8 | |||||
| 143 | ||||||
| 144 - 154 | =head1 NAME Test::Mockingbird - Advanced mocking library for Perl with support for dependency injection, spies, call ordering, constructor interception, and async Future mocking =head1 VERSION Version 0.14 =cut | |||||
| 155 | ||||||
| 156 | our $VERSION = '0.14'; | |||||
| 157 | ||||||
| 158 - 320 | =head1 SYNOPSIS
use Test::Mockingbird;
# Mocking (shorthand form)
mock 'My::Module::method' => sub { 'mocked' };
# Mocking (longhand form)
mock('My::Module', 'method', sub { 'mocked' });
# Spying
my $spy = spy 'My::Module::method';
My::Module::method('arg1');
my @calls = $spy->(); # ( ['My::Module::method', 'arg1'], ... )
# Dependency injection
inject 'My::Module::Dependency' => $mock_object;
# Batch dependency injection
inject_all('My::Module', {
DB => $mock_db,
Logger => $mock_logger,
});
# Constructor interception
intercept_new 'My::Service' => $stub_obj;
intercept_new 'My::Service' => sub { My::Double->new(@_[1..$#_]) };
# Unmock one layer
unmock 'My::Module::method';
# Restore everything
restore_all();
# Call ordering
spy 'A::fetch';
spy 'B::process';
A::fetch();
B::process();
assert_call_order('A::fetch', 'B::process');
clear_call_log();
=head1 DESCRIPTION
Test::Mockingbird provides mocking, spying, dependency injection,
call-order verification, and constructor interception for Perl test suites.
=head1 DIAGNOSTICS
L</diagnose_mocks> returns a structured hashref of all active mock layers;
L</diagnose_mocks_pretty> returns the same as a human-readable multi-line
string.
Each installed layer records:
type -- category (mock, spy, inject, mock_return, ...)
installed_at -- file and line number of the outermost user call site
=head1 LIMITATIONS
=over 4
=item A misspelled method name is mocked without complaint
C<mock 'My::Module::fecth' =E<gt> sub { ... }> installs a new sub called
C<fecth>; the real C<fetch> is left untouched and a test relying on the mock
may pass for the wrong reason. Check with C<can> (or C<diagnose_mocks()>)
when a mock seems to have no effect.
=item Names with non-ASCII characters
Package and method names are used as given, so any name Perl accepts under
C<use utf8> (e.g. C<Café::prix>) can be mocked, spied on, injected and
restored. C<mock_core()> only accepts ASCII builtin names, which covers
every Perl builtin.
=item Call-through to an inherited method does not consult C<AUTOLOAD>
When C<spy()>, C<before()>, C<after()>, C<around()> or C<async_spy()> wrap a
method the package does not declare itself, they call through to the first
implementation found in the package's parent classes (looked up at call
time, so a later mock of the parent is seen). If there is none they die with
C<"Undefined subroutine">; an C<AUTOLOAD> is not tried.
=item Prototype mismatch warning from C<spy()>
C<spy()> installs its wrapper directly without going through C<mock()>,
so C<Scalar::Util::set_prototype> is not applied. Wrapping a prototyped
function with C<spy()> still emits a C<Prototype mismatch> warning. Use
C<mock()> with a delegating wrapper if warning-free wrapping is required.
=item No nested deep_mock scopes
L<Test::Mockingbird::DeepMock> calls C<restore_all()> at scope exit, which
removes every active mock. Nested C<deep_mock> blocks cause the inner exit
to also tear down the outer mocks. Do not nest C<deep_mock> calls.
=item Thread safety
The internal state (C<%mocked>, C<%mock_meta>, C<@call_log>) is per-process
lexical state. Concurrent threads that install and restore mocks will race.
Do not use this module in threaded test harnesses without external locking.
=item Spy return value is a flat list
C<spy()> and C<async_spy()> return a coderef that yields a flat list of
call records. A future version may return an arrayref to reduce stack
pressure; the API is not yet changed to avoid breaking callers.
=item Private-function encapsulation
Functions prefixed with C<_> are private by convention but are not enforced
at runtime (C<Sub::Private> is not activated). White-box tests in C<t/unit.t>
call private functions directly. If C<Sub::Private> enforcement is added, a
testing-interface export mechanism will be required.
=back
=head1 METHODS
=head2 mock
Replace a method with a coderef.
mock('My::Module', 'method', sub { 'mocked' });
mock 'My::Module::method' => sub { 'mocked' };
Mocks stack in LIFO order. Each C<mock()> call saves the current CODE slot
(or notes that there is none, if the package does not declare the method)
and installs the replacement. C<unmock()> pops one layer; C<restore_all()>
drains all.
A method the package only inherits can be mocked in that package alone:
mock 'My::Child::greet' => sub { 'mocked' }; # My::Parent unaffected
unmock 'My::Child::greet'; # inherits again
Removing the last layer empties the package's CODE slot, so method lookup
reaches the parent class again, exactly as before the mock.
If the original carries a Perl prototype, the same prototype is stamped onto
the replacement coderef before installation, suppressing C<Prototype mismatch>
warnings.
=head3 API SPECIFICATION
=head4 Input
target -- Str, 'Pkg::method' or ('Pkg', 'method')
replacement -- CodeRef
=head4 Output
returns: undef
=head3 MESSAGES
"Package, method and replacement are required for mocking"
-- package, method or replacement missing (undef, '' or false)
"mock: replacement for 'Pkg::method' must be a coderef"
-- replacement is a string or a non-CODE reference
=cut | |||||
| 321 | ||||||
| 322 | sub mock { | |||||
| 323 | 505 | 635312 | my ($arg1, $arg2, $arg3) = @_; | |||
| 324 | ||||||
| 325 | 505 | 323 | my ($package, $method, $replacement); | |||
| 326 | ||||||
| 327 | # Shorthand: 'Pkg::method' => $code (arg3 absent) | |||||
| 328 | 505 | 1397 | if (defined $arg1 && !defined $arg3 && $arg1 =~ /^(.*)::([^:]+)$/) { | |||
| 329 | 321 | 406 | ($package, $method, $replacement) = ($1, $2, $arg2); | |||
| 330 | } else { | |||||
| 331 | 184 | 155 | ($package, $method, $replacement) = ($arg1, $arg2, $arg3); | |||
| 332 | } | |||||
| 333 | ||||||
| 334 | 505 | 412 | croak 'Package, method and replacement are required for mocking' | |||
| 335 | unless _is_name($package) && _is_name($method) && $replacement; | |||||
| 336 | ||||||
| 337 | # Anything but a coderef is silently installed wrongly by the glob | |||||
| 338 | # assignment below: a string aliases the whole typeglob to another | |||||
| 339 | # symbol and a reference fills the wrong slot, leaving the method real. | |||||
| 340 | 486 | 510 | croak "mock: replacement for '${package}::${method}' must be a coderef" | |||
| 341 | unless ref($replacement) eq 'CODE'; | |||||
| 342 | ||||||
| 343 | 482 | 313 | my $full_method = "${package}::${method}"; | |||
| 344 | ||||||
| 345 | # Capture the current CODE slot. When no sub of that name has been | |||||
| 346 | # declared in the package, undef is saved instead: on restore the CODE | |||||
| 347 | # slot is emptied (see _restore_slot) so that inherited methods resolve | |||||
| 348 | # through @ISA again. Restoration always writes back to the SAME GV that | |||||
| 349 | # compiled direct calls hold, never deleting it. | |||||
| 350 | 482 | 385 | my ($orig_existed, $original) = _capture_slot($full_method); | |||
| 351 | 482 482 | 285 484 | push @{ $mocked{$full_method} }, $original; | |||
| 352 | ||||||
| 353 | # Stamp the prototype onto the replacement to avoid Perl warning about | |||||
| 354 | # "Prototype mismatch" when the original had a prototype. | |||||
| 355 | 482 | 422 | my $orig_proto = defined $original ? prototype($original) : undef; | |||
| 356 | 482 | 384 | if (defined $orig_proto) { | |||
| 357 | 16 | 37 | &Scalar::Util::set_prototype($replacement, $orig_proto); | |||
| 358 | } | |||||
| 359 | ||||||
| 360 | { | |||||
| 361 | # 'redefine' suppresses "Subroutine ... redefined". | |||||
| 362 | # 'prototype' suppresses "Prototype mismatch" -- that warning lives in | |||||
| 363 | # a separate category and is not covered by 'redefine'. set_prototype() | |||||
| 364 | # above should already make the prototypes equal, but on some Perl builds | |||||
| 365 | # the GV-level check still fires before the CV slot is fully updated, so | |||||
| 366 | # we suppress the warning here. | |||||
| 367 | 26 26 26 482 | 106 16 507 267 | no warnings 'redefine', 'prototype'; | |||
| 368 | 26 26 26 | 62 23 17036 | no strict 'refs'; ## no critic (ProhibitNoStrict) | |||
| 369 | 482 482 | 249 657 | *{$full_method} = $replacement; | |||
| 370 | } | |||||
| 371 | ||||||
| 372 | 482 482 | 298 686 | push @{ $mock_meta{$full_method} }, { | |||
| 373 | type => $TYPE // _T_MOCK, | |||||
| 374 | installed_at => _caller_info(), | |||||
| 375 | original_existed => $orig_existed, | |||||
| 376 | }; | |||||
| 377 | ||||||
| 378 | 482 | 569 | return; | |||
| 379 | } | |||||
| 380 | ||||||
| 381 - 409 | =head2 unmock
Restore the previous implementation of a mocked method (one layer).
unmock('My::Module', 'method');
unmock 'My::Module::method';
If the package did not declare the method before it was mocked, removing
the last layer empties its CODE slot: an inherited method is found through
C<@ISA> again, and a method defined nowhere is once more undefined (a direct
call dies with C<"Undefined subroutine"> and C<< ->can() >> returns false).
The typeglob itself and its other slots (e.g. a package variable of the same
name) are kept, so previously compiled direct calls still see later mocks.
=head3 API SPECIFICATION
=head4 Input
target -- Str, 'Pkg::method' or ('Pkg', 'method')
=head4 Output
returns: undef
=head3 MESSAGES
"Package and method are required for unmocking" -- target missing
=cut | |||||
| 410 | ||||||
| 411 | sub unmock { | |||||
| 412 | 139 | 24305 | my ($arg1, $arg2) = @_; | |||
| 413 | ||||||
| 414 | 139 | 121 | my ($package, $method); | |||
| 415 | 139 | 513 | if (defined $arg1 && !defined $arg2 && $arg1 =~ /^(.*)::([^:]+)$/) { | |||
| 416 | 103 | 151 | ($package, $method) = ($1, $2); | |||
| 417 | } else { | |||||
| 418 | 36 | 43 | ($package, $method) = ($arg1, $arg2); | |||
| 419 | } | |||||
| 420 | ||||||
| 421 | 139 | 134 | croak 'Package and method are required for unmocking' | |||
| 422 | unless _is_name($package) && _is_name($method); | |||||
| 423 | ||||||
| 424 | 133 | 141 | my $full_method = "${package}::${method}"; | |||
| 425 | ||||||
| 426 | # Nothing to do if this method was never mocked | |||||
| 427 | 133 128 | 197 179 | return unless exists $mocked{$full_method} && @{ $mocked{$full_method} }; | |||
| 428 | ||||||
| 429 | 128 128 | 105 179 | _restore_slot($full_method, pop @{ $mocked{$full_method} }); | |||
| 430 | ||||||
| 431 | # Pop exactly one meta entry to mirror the mock stack. | |||||
| 432 | # Earlier code deleted the entire key; that wiped meta for all layers | |||||
| 433 | # still on the stack after a partial unmock. | |||||
| 434 | 128 128 | 78 133 | pop @{ $mock_meta{$full_method} }; | |||
| 435 | ||||||
| 436 | # Clean up empty tracking structures | |||||
| 437 | 128 128 | 130 159 | unless (@{ $mocked{$full_method} }) { | |||
| 438 | 105 | 74 | delete $mocked{$full_method}; | |||
| 439 | 105 | 111 | delete $mock_meta{$full_method}; | |||
| 440 | } | |||||
| 441 | ||||||
| 442 | 128 | 135 | return; | |||
| 443 | } | |||||
| 444 | ||||||
| 445 - 476 | =head2 before
Run a hook before a method, then call the original and return its value.
before 'My::Module::method' => sub { my @args = @_; ... };
before('My::Module', 'method', sub { ... });
The hook receives the same C<@_> that the original would have received. Its
return value is discarded. The original is always called and its return value
is passed to the caller unchanged. Context (list / scalar / void) is
preserved.
Uses the same LIFO mock stack as C<mock()>: C<unmock()> peels one layer,
C<restore_all()> drains all. C<diagnose_mocks()> records the layer type as
C<'before'>.
=head3 API SPECIFICATION
=head4 Input
target -- Str, 'Pkg::method' or ('Pkg', 'method')
hook -- CodeRef; receives (@original_args), return value discarded
=head4 Output
returns: undef
=head3 MESSAGES
"Package, method and hook are required for before()" -- target or hook missing or non-CODE
=cut | |||||
| 477 | ||||||
| 478 | sub before { | |||||
| 479 | 38 | 112126 | my ($arg1, $arg2, $arg3) = @_; | |||
| 480 | ||||||
| 481 | 38 | 28 | my ($package, $method, $hook); | |||
| 482 | 38 | 160 | if (defined $arg1 && !defined $arg3 && $arg1 =~ /^(.*)::([^:]+)$/) { | |||
| 483 | 31 | 45 | ($package, $method, $hook) = ($1, $2, $arg2); | |||
| 484 | } else { | |||||
| 485 | 7 | 9 | ($package, $method, $hook) = ($arg1, $arg2, $arg3); | |||
| 486 | } | |||||
| 487 | ||||||
| 488 | 38 | 41 | croak 'Package, method and hook are required for before()' | |||
| 489 | unless _is_name($package) && _is_name($method) && ref($hook) eq 'CODE'; | |||||
| 490 | ||||||
| 491 | 32 | 31 | my $full_method = "${package}::${method}"; | |||
| 492 | 32 | 32 | my $orig = _call_through($package, $method); | |||
| 493 | ||||||
| 494 | 32 | 27 | local $TYPE = _T_BEFORE; | |||
| 495 | mock($package, $method, sub { | |||||
| 496 | 29 | 92 | my @args = @_; | |||
| 497 | 29 | 29 | $hook->(@args); | |||
| 498 | 28 | 52 | if (wantarray) { | |||
| 499 | 3 | 3 | return $orig->(@args); | |||
| 500 | } elsif (defined wantarray) { | |||||
| 501 | 15 | 14 | return scalar $orig->(@args); | |||
| 502 | } else { | |||||
| 503 | 10 | 12 | $orig->(@args); | |||
| 504 | 10 | 13 | return; | |||
| 505 | } | |||||
| 506 | 32 | 59 | }); | |||
| 507 | ||||||
| 508 | 32 | 39 | return; | |||
| 509 | } | |||||
| 510 | ||||||
| 511 - 546 | =head2 after
Run a hook after a method and return the original's value.
after 'My::Module::method' => sub { my @args = @_; ... };
after('My::Module', 'method', sub { ... });
The original is called first. Its return value is captured, then the hook is
called with the same C<@_> that the original received. The hook's return
value is discarded and the original's return value is passed to the caller
unchanged. Context (list / scalar / void) is preserved.
If the original throws, the exception propagates immediately and the hook is
B<not> called. Use C<around()> if you need to run code unconditionally after
the original.
Uses the same LIFO mock stack as C<mock()>: C<unmock()> peels one layer,
C<restore_all()> drains all. C<diagnose_mocks()> records the layer type as
C<'after'>.
=head3 API SPECIFICATION
=head4 Input
target -- Str, 'Pkg::method' or ('Pkg', 'method')
hook -- CodeRef; receives (@original_args), return value discarded
=head4 Output
returns: undef
=head3 MESSAGES
"Package, method and hook are required for after()" -- target or hook missing or non-CODE
=cut | |||||
| 547 | ||||||
| 548 | sub after { | |||||
| 549 | 37 | 27202 | my ($arg1, $arg2, $arg3) = @_; | |||
| 550 | ||||||
| 551 | 37 | 35 | my ($package, $method, $hook); | |||
| 552 | 37 | 149 | if (defined $arg1 && !defined $arg3 && $arg1 =~ /^(.*)::([^:]+)$/) { | |||
| 553 | 30 | 45 | ($package, $method, $hook) = ($1, $2, $arg2); | |||
| 554 | } else { | |||||
| 555 | 7 | 6 | ($package, $method, $hook) = ($arg1, $arg2, $arg3); | |||
| 556 | } | |||||
| 557 | ||||||
| 558 | 37 | 57 | croak 'Package, method and hook are required for after()' | |||
| 559 | unless _is_name($package) && _is_name($method) && ref($hook) eq 'CODE'; | |||||
| 560 | ||||||
| 561 | 31 | 29 | my $full_method = "${package}::${method}"; | |||
| 562 | 31 | 33 | my $orig = _call_through($package, $method); | |||
| 563 | ||||||
| 564 | 31 | 27 | local $TYPE = _T_AFTER; | |||
| 565 | mock($package, $method, sub { | |||||
| 566 | 30 | 132 | my @args = @_; | |||
| 567 | 30 | 36 | if (wantarray) { | |||
| 568 | 3 | 4 | my @ret = $orig->(@args); | |||
| 569 | 3 | 7 | $hook->(@args); | |||
| 570 | 3 | 6 | return @ret; | |||
| 571 | } elsif (defined wantarray) { | |||||
| 572 | 17 | 19 | my $ret = $orig->(@args); | |||
| 573 | 17 | 31 | $hook->(@args); | |||
| 574 | 16 | 28 | return $ret; | |||
| 575 | } else { | |||||
| 576 | 10 | 11 | $orig->(@args); | |||
| 577 | 8 | 21 | $hook->(@args); | |||
| 578 | 8 | 14 | return; | |||
| 579 | } | |||||
| 580 | 31 | 67 | }); | |||
| 581 | ||||||
| 582 | 31 | 31 | return; | |||
| 583 | } | |||||
| 584 | ||||||
| 585 - 629 | =head2 around
Replace a method with a hook that receives the original coderef as its first
argument.
around 'My::Module::method' => sub {
my ($orig, @args) = @_;
my $result = $orig->(@args); # call original
return $result * 2; # modify return value
};
around('My::Module', 'method', sub {
my ($orig, @args) = @_;
return $orig->(@args);
});
The hook receives C<($orig_coderef, @original_args)>. It may call C<$orig>
zero or more times with any arguments. Its return value becomes the return
value of the method. The hook is responsible for context handling when that
matters.
C<around()> is the preferred alternative to C<mock()> when you need to call
through to the original: it captures the original and passes it as the first
argument, avoiding the boilerplate of a separate C<\&{...}> capture.
Uses the same LIFO mock stack as C<mock()>: C<unmock()> peels one layer,
C<restore_all()> drains all. C<diagnose_mocks()> records the layer type as
C<'around'>.
=head3 API SPECIFICATION
=head4 Input
target -- Str, 'Pkg::method' or ('Pkg', 'method')
hook -- CodeRef; receives ($orig_coderef, @original_args)
=head4 Output
returns: undef
=head3 MESSAGES
"Package, method and hook are required for around()" -- target or hook missing or non-CODE
=cut | |||||
| 630 | ||||||
| 631 | sub around { | |||||
| 632 | 42 | 28135 | my ($arg1, $arg2, $arg3) = @_; | |||
| 633 | ||||||
| 634 | 42 | 61 | my ($package, $method, $hook); | |||
| 635 | 42 | 170 | if (defined $arg1 && !defined $arg3 && $arg1 =~ /^(.*)::([^:]+)$/) { | |||
| 636 | 34 | 48 | ($package, $method, $hook) = ($1, $2, $arg2); | |||
| 637 | } else { | |||||
| 638 | 8 | 9 | ($package, $method, $hook) = ($arg1, $arg2, $arg3); | |||
| 639 | } | |||||
| 640 | ||||||
| 641 | 42 | 41 | croak 'Package, method and hook are required for around()' | |||
| 642 | unless _is_name($package) && _is_name($method) && ref($hook) eq 'CODE'; | |||||
| 643 | ||||||
| 644 | 35 | 32 | my $full_method = "${package}::${method}"; | |||
| 645 | 35 | 31 | my $orig = _call_through($package, $method); | |||
| 646 | ||||||
| 647 | 35 | 27 | local $TYPE = _T_AROUND; | |||
| 648 | 35 32 | 73 669 | mock($package, $method, sub { $hook->($orig, @_) }); | |||
| 649 | ||||||
| 650 | 35 | 38 | return; | |||
| 651 | } | |||||
| 652 | ||||||
| 653 - 691 | =head2 mock_scoped
Create a scoped mock that restores automatically when the guard goes out of scope.
=head3 Single-method forms
my $g = mock_scoped 'My::Module::method' => sub { 'mocked' };
my $g = mock_scoped('My::Module', 'method', sub { ... });
=head3 Multi-method forms
my $g = mock_scoped('My::Module',
fetch => sub { 'mocked_fetch' },
save => sub { 'mocked_save' },
);
my $g = mock_scoped(
'My::Module::fetch' => sub { 'mocked_fetch' },
'Other::Module::save' => sub { 'mocked_save' },
);
All mocked methods are restored when C<$g> goes out of scope.
=head3 API SPECIFICATION
=head4 Input
args -- four recognised forms (see above)
=head4 Output
returns: Test::Mockingbird::Guard
=head3 MESSAGES
"mock_scoped: unrecognised argument form" -- none of the four forms matched
"mock_scoped: expected coderef for '$target'" -- non-CODE value provided
=cut | |||||
| 692 | ||||||
| 693 | sub mock_scoped { | |||||
| 694 | 41 | 209737 | my @args = @_; | |||
| 695 | ||||||
| 696 | 41 | 37 | my @pairs; | |||
| 697 | ||||||
| 698 | 41 | 226 | if (@args == 2 && ref($args[1]) eq 'CODE') { | |||
| 699 | 14 | 24 | my ($pkg, $meth) = _parse_target($args[0]); | |||
| 700 | 14 | 25 | push @pairs, [ $pkg, $meth, $args[1] ]; | |||
| 701 | ||||||
| 702 | } elsif (@args == 3 && !ref($args[1]) && ref($args[2]) eq 'CODE') { | |||||
| 703 | 4 | 7 | push @pairs, [ $args[0], $args[1], $args[2] ]; | |||
| 704 | ||||||
| 705 | } elsif (@args >= 4 && (@args % 2) == 0 && ref($args[1]) eq 'CODE') { | |||||
| 706 | 5 | 7 | my @a = @args; | |||
| 707 | 5 | 7 | while (@a) { | |||
| 708 | 11 | 13 | my ($target, $code) = splice @a, 0, 2; | |||
| 709 | 11 | 20 | croak "mock_scoped: expected coderef for '$target'" | |||
| 710 | unless ref($code) eq 'CODE'; | |||||
| 711 | 10 | 10 | my ($pkg, $meth) = _parse_target($target); | |||
| 712 | 10 | 15 | push @pairs, [ $pkg, $meth, $code ]; | |||
| 713 | } | |||||
| 714 | ||||||
| 715 | } elsif (@args >= 5 && (@args % 2) == 1 && ref($args[2]) eq 'CODE') { | |||||
| 716 | 7 | 11 | my @a = @args; | |||
| 717 | 7 | 36 | my $pkg = shift @a; | |||
| 718 | 7 | 17 | while (@a) { | |||
| 719 | 15 | 20 | my ($meth, $code) = splice @a, 0, 2; | |||
| 720 | 15 | 66 | croak "mock_scoped: expected coderef for method '$meth'" | |||
| 721 | unless ref($code) eq 'CODE'; | |||||
| 722 | 14 | 24 | push @pairs, [ $pkg, $meth, $code ]; | |||
| 723 | } | |||||
| 724 | ||||||
| 725 | } else { | |||||
| 726 | 11 | 43 | croak 'mock_scoped: unrecognised argument form'; | |||
| 727 | } | |||||
| 728 | ||||||
| 729 | 28 | 23 | my @full_methods; | |||
| 730 | { | |||||
| 731 | 28 28 | 22 27 | local $TYPE = _T_MOCK_SCOPED; | |||
| 732 | 28 | 28 | for my $pair (@pairs) { | |||
| 733 | 40 40 | 29 60 | my ($pkg, $meth, $code) = @{$pair}; | |||
| 734 | 40 | 62 | mock($pkg, $meth, $code); | |||
| 735 | 40 | 55 | push @full_methods, "${pkg}::${meth}"; | |||
| 736 | } | |||||
| 737 | } | |||||
| 738 | ||||||
| 739 | 28 | 59 | return Test::Mockingbird::Guard->new(@full_methods); | |||
| 740 | } | |||||
| 741 | ||||||
| 742 - 769 | =head2 spy
Wrap a method so that every call is recorded. The original method is still
called and its return value is passed back to the caller.
my $spy = spy 'My::Module::method';
My::Module::method('arg');
my @calls = $spy->(); # ( ['My::Module::method', 'arg'], ... )
restore_all();
Returns a coderef that, when invoked, returns the list of captured call
records. Each record is an arrayref C<[ $full_method, @args ]>.
=head3 API SPECIFICATION
=head4 Input
target -- Str, 'Pkg::method' or ('Pkg', 'method')
=head4 Output
returns: CodeRef # yields list of call records on invocation
=head3 MESSAGES
"Package and method are required for spying" -- target missing or incomplete
=cut | |||||
| 770 | ||||||
| 771 | sub spy { | |||||
| 772 | 131 | 223603 | my ($package, $method) = _parse_target(@_); | |||
| 773 | ||||||
| 774 | 131 | 130 | croak 'Package and method are required for spying' | |||
| 775 | unless _is_name($package) && _is_name($method); | |||||
| 776 | ||||||
| 777 | 126 | 120 | my $full_method = "${package}::${method}"; | |||
| 778 | ||||||
| 779 | # Save the current CODE slot for restoration (see mock()), and resolve | |||||
| 780 | # what the wrapper calls through to -- which, for a method the package | |||||
| 781 | # only inherits, is the parent's implementation. | |||||
| 782 | 126 | 112 | my ($orig_existed, $saved) = _capture_slot($full_method); | |||
| 783 | 126 126 | 89 136 | push @{ $mocked{$full_method} }, $saved; | |||
| 784 | 126 | 127 | my $orig = _call_through($package, $method); | |||
| 785 | ||||||
| 786 | 126 | 75 | my @calls; | |||
| 787 | ||||||
| 788 | my $wrapper = sub { | |||||
| 789 | 142 | 2106 | push @calls, [ $full_method, @_ ]; | |||
| 790 | 142 | 91 | push @call_log, $full_method; | |||
| 791 | # A recursive method re-enters this wrapper and is recorded once per | |||||
| 792 | # call, outermost first (t/unit.t: "spy(): recursive calls"). | |||||
| 793 | 142 | 134 | return $orig->(@_); | |||
| 794 | 126 | 200 | }; | |||
| 795 | ||||||
| 796 | # Preserve the original's prototype to suppress "Prototype mismatch" | |||||
| 797 | # warnings and ensure '_'-prototype functions (stat, lstat, etc.) bind | |||||
| 798 | # $_ correctly at the call site when wrapped. | |||||
| 799 | 126 | 123 | my $orig_proto = prototype($orig); | |||
| 800 | 126 | 100 | if (defined $orig_proto) { | |||
| 801 | 1 | 2 | &Scalar::Util::set_prototype($wrapper, $orig_proto); | |||
| 802 | } | |||||
| 803 | ||||||
| 804 | { | |||||
| 805 | 26 26 26 126 | 92 25 513 71 | no warnings 'redefine', 'prototype'; | |||
| 806 | 26 26 26 | 77 24 3130 | no strict 'refs'; ## no critic (ProhibitNoStrict) | |||
| 807 | 126 126 | 70 165 | *{$full_method} = $wrapper; | |||
| 808 | } | |||||
| 809 | ||||||
| 810 | 126 126 | 83 150 | push @{ $mock_meta{$full_method} }, { | |||
| 811 | type => _T_SPY, | |||||
| 812 | installed_at => _caller_info(), | |||||
| 813 | original_existed => $orig_existed, | |||||
| 814 | }; | |||||
| 815 | ||||||
| 816 | 126 51 | 201 2687 | return sub { @calls }; | |||
| 817 | } | |||||
| 818 | ||||||
| 819 - 846 | =head2 inject
Inject a mock dependency into a package.
inject('My::Module', 'Dependency', $mock_object);
inject 'My::Module::Dependency' => $mock_object;
Injecting C<undef> is valid; use argument count (not definedness of the
third argument) to distinguish shorthand from longhand.
=head3 API SPECIFICATION
=head4 Input
package -- Str
dependency -- Str
value -- Any (including undef)
=head4 Output
returns: undef
=head3 MESSAGES
"Package and dependency are required for injection" -- missing name, or a
two-argument call whose target has no '::'
=cut | |||||
| 847 | ||||||
| 848 | sub inject { | |||||
| 849 | 59 | 107654 | my ($package, $dependency, $mock_object); | |||
| 850 | ||||||
| 851 | # Discriminate shorthand (2 args) from longhand (3 args) by argument | |||||
| 852 | # count rather than definedness of the third arg so that inject(Pkg, | |||||
| 853 | # Dep, undef) -- injecting undef -- is correctly handled. | |||||
| 854 | # Two arguments are always the shorthand form; a target without '::' | |||||
| 855 | # leaves $package undef and croaks below, rather than being read as | |||||
| 856 | # longhand and silently injecting undef under the value's name. | |||||
| 857 | 59 | 69 | if (@_ == 2) { | |||
| 858 | 14 | 46 | ($package, $dependency) = $_[0] =~ /^(.*)::([^:]+)$/ if defined $_[0]; | |||
| 859 | 14 | 15 | $mock_object = $_[1]; | |||
| 860 | } else { | |||||
| 861 | 45 | 44 | ($package, $dependency, $mock_object) = @_; | |||
| 862 | } | |||||
| 863 | ||||||
| 864 | 59 | 56 | croak 'Package and dependency are required for injection' | |||
| 865 | unless _is_name($package) && _is_name($dependency); | |||||
| 866 | ||||||
| 867 | 49 | 46 | my $full = "${package}::${dependency}"; | |||
| 868 | ||||||
| 869 | 49 | 47 | my ($orig_existed, $orig) = _capture_slot($full); | |||
| 870 | 49 49 | 34 57 | push @{ $mocked{$full} }, $orig; | |||
| 871 | ||||||
| 872 | 49 36 | 65 704 | my $wrapper = sub { $mock_object }; | |||
| 873 | ||||||
| 874 | { | |||||
| 875 | 26 26 26 49 | 101 45 423 32 | no warnings 'redefine'; | |||
| 876 | 26 26 26 | 62 24 6230 | no strict 'refs'; ## no critic (ProhibitNoStrict) | |||
| 877 | 49 49 | 34 65 | *{$full} = $wrapper; | |||
| 878 | } | |||||
| 879 | ||||||
| 880 | # inject() respects $TYPE so that inject_all() or any future wrapper | |||||
| 881 | # can label the layer differently (though 'inject' is the sensible default). | |||||
| 882 | 49 49 | 36 102 | push @{ $mock_meta{$full} }, { | |||
| 883 | type => $TYPE // _T_INJECT, | |||||
| 884 | installed_at => _caller_info(), | |||||
| 885 | original_existed => $orig_existed, | |||||
| 886 | }; | |||||
| 887 | ||||||
| 888 | 49 | 72 | return; | |||
| 889 | } | |||||
| 890 | ||||||
| 891 - 919 | =head2 inject_all
Inject multiple dependencies into a package in one call.
inject_all('My::Service', {
DB => $mock_db,
Logger => $mock_logger,
});
An empty hashref is a no-op. Each pair is equivalent to a separate
C<inject()> call and participates in the same mock stack.
=head3 API SPECIFICATION
=head4 Input
package -- Str
dependencies -- HashRef
=head4 Output
returns: undef
=head3 MESSAGES
"inject_all requires a package name" -- undef or empty package
"inject_all requires a hashref of dependencies" -- second arg not a HashRef
=cut | |||||
| 920 | ||||||
| 921 | sub inject_all { | |||||
| 922 | 27 | 95028 | my ($package, $deps) = @_; | |||
| 923 | ||||||
| 924 | 27 | 96 | croak 'inject_all requires a package name' | |||
| 925 | unless defined $package && length $package; | |||||
| 926 | ||||||
| 927 | 19 | 43 | croak 'inject_all requires a hashref of dependencies' | |||
| 928 | unless ref $deps eq 'HASH'; | |||||
| 929 | ||||||
| 930 | 12 | 21 | inject($package, $_, $deps->{$_}) for keys %$deps; | |||
| 931 | ||||||
| 932 | 12 | 16 | return; | |||
| 933 | } | |||||
| 934 | ||||||
| 935 - 966 | =head2 intercept_new
Intercept the C<new> constructor of a class.
intercept_new 'My::Service' => $stub_obj;
intercept_new 'My::Service' => sub { My::Double->new(@_[1..$#_]) };
When given a plain value (including undef), every call to
C<< My::Service->new >> returns that value. When given a coderef, every
call invokes the coderef with the original arguments (including the class
name as the first argument) and returns its result.
This is a thin wrapper around C<mock()>; C<restore_all()>, C<unmock()>,
and C<diagnose_mocks()> all work identically.
=head3 API SPECIFICATION
=head4 Input
class -- Str (non-empty)
factory -- Any; CodeRef invoked per call, or scalar returned verbatim
=head4 Output
returns: undef
=head3 MESSAGES
"intercept_new requires a class name" -- undef/empty class
"intercept_new requires a replacement object or coderef" -- factory missing
=cut | |||||
| 967 | ||||||
| 968 | sub intercept_new { | |||||
| 969 | 37 | 115855 | my ($class, $factory) = @_; | |||
| 970 | ||||||
| 971 | 37 | 114 | croak 'intercept_new requires a class name' | |||
| 972 | unless defined $class && length $class; | |||||
| 973 | 29 | 44 | croak 'intercept_new requires a replacement object or coderef' | |||
| 974 | if @_ < 2; | |||||
| 975 | ||||||
| 976 | my $replacement = ref($factory) eq 'CODE' | |||||
| 977 | ? $factory | |||||
| 978 | 25 20 | 42 51 | : sub { $factory }; | |||
| 979 | ||||||
| 980 | 25 | 34 | local $TYPE = _T_INTERCEPT_NEW; | |||
| 981 | 25 | 36 | mock("${class}::new", $replacement); | |||
| 982 | ||||||
| 983 | 25 | 26 | return; | |||
| 984 | } | |||||
| 985 | ||||||
| 986 - 1054 | =head2 mock_core
Override a CORE Perl builtin globally via C<CORE::GLOBAL>.
# Intercept 'warn' for code compiled after this point
mock_core 'warn' => sub {
my ($call_warn, @msgs) = @_;
push @captured, @msgs; # capture without emitting
};
# Call through to the real builtin via $call_builtin
mock_core 'stat' => sub {
my ($call_stat, $file) = @_;
return $call_stat->($file); # delegates to CORE::stat
};
unmock 'CORE::GLOBAL::warn'; # peel one layer
restore_all(); # drain all layers
The replacement receives C<($call_builtin, @original_args)>, mirroring the
C<around()> API. C<$call_builtin> is a coderef that calls C<CORE::$name>
directly, bypassing any other C<CORE::GLOBAL> override. Builtins with a
fixed argument list, such as C<time>, C<index> and C<substr>, get their
arguments passed by position. A few builtins take arguments that no coderef
can pass on (C<tie>, C<pos>, C<dbmopen>, C<dbmclose>): they can still be
mocked, but calling C<$call_builtin> croaks.
The override is installed in C<CORE::GLOBAL::$name>, which is Perl's
documented mechanism for intercepting named builtins. It affects all
packages globally.
B<Compile-time semantics:> C<CORE::GLOBAL> overrides are visible to code
compiled I<after> the override is installed. To intercept calls in a module
under test, install the mock I<before> loading that module (a C<BEGIN> block
works). Already-compiled call sites (including direct calls in the current
test file) are not affected at runtime. Use string C<eval> when you need
code compiled in the same test run to see the override.
The wrapper carries the same prototype as C<CORE::$name> so that call-site
argument binding (such as the C<_> prototype that reads C<$_> when no
argument is given) is preserved.
Participates in the same LIFO mock stack as C<mock()>. C<unmock>,
C<restore()>, and C<restore_all()> accept C<'CORE::GLOBAL::$name'> as the
target. C<diagnose_mocks()> records the layer type as C<'mock_core'>.
B<Limitation:> builtins whose prototype begins with C<&> (C<sort>, C<map>,
C<grep>) require a literal code block at the call site and cannot be wrapped.
=head3 API SPECIFICATION
=head4 Input
name -- Str, CORE builtin name (no 'CORE::' prefix required)
replacement -- CodeRef; receives ($call_builtin, @original_args)
=head4 Output
returns: undef
=head3 MESSAGES
"mock_core requires a builtin name and a replacement coderef" -- wrong arg types
"mock_core: '$name' is not a valid identifier" -- name has punctuation
"mock_core: '$name' is not an overridable Perl builtin" -- unknown builtin
"mock_core: cannot call through to CORE::$name: ..." -- $call_builtin
called for a builtin whose arguments cannot be delegated
=cut | |||||
| 1055 | ||||||
| 1056 | sub mock_core { | |||||
| 1057 | 22 | 93313 | my ($name, $replacement) = @_; | |||
| 1058 | ||||||
| 1059 | 22 | 55 | croak 'mock_core requires a builtin name and a replacement coderef' | |||
| 1060 | unless defined $name && ref($replacement) eq 'CODE'; | |||||
| 1061 | ||||||
| 1062 | 19 | 15 | $name =~ s/^CORE:://; # tolerate an optional 'CORE::' prefix | |||
| 1063 | ||||||
| 1064 | 19 | 37 | croak "mock_core: '$name' is not a valid identifier" | |||
| 1065 | unless $name =~ /^\w+$/; | |||||
| 1066 | 18 | 19 | croak "mock_core: '$name' is not an overridable Perl builtin" | |||
| 1067 | unless _is_core_overridable($name); | |||||
| 1068 | ||||||
| 1069 | 17 17 17 | 14 34 14 | my $core_proto = eval { my $p = prototype("CORE::$name"); $p }; | |||
| 1070 | 17 | 19 | my $call_core = _core_delegator($name, $core_proto); | |||
| 1071 | ||||||
| 1072 | # Wrap in an around-style closure and stamp the CORE prototype onto it | |||||
| 1073 | # so call-site argument binding (e.g. '_' reads $_ when arg omitted) works. | |||||
| 1074 | # List-op builtins (warn, die, print ...) have no traditional prototype | |||||
| 1075 | # string (prototype("CORE::warn") returns undef), but Perl's CORE::GLOBAL | |||||
| 1076 | # mechanism expects the override to carry '@'. Use '@' as the fallback so | |||||
| 1077 | # the wrapper's prototype matches and suppresses "Prototype mismatch" at | |||||
| 1078 | # eval-compile time. | |||||
| 1079 | 17 16 | 22 1016 | my $wrapper = sub { $replacement->($call_core, @_) }; | |||
| 1080 | 17 | 21 | my $effective_proto = defined($core_proto) ? $core_proto : '@'; | |||
| 1081 | 17 | 40 | &Scalar::Util::set_prototype($wrapper, $effective_proto); | |||
| 1082 | ||||||
| 1083 | # Install in CORE::GLOBAL -- Perl's documented mechanism for global | |||||
| 1084 | # builtin overrides. Track under the full name so unmock/restore_all work. | |||||
| 1085 | 17 | 14 | my $full = "CORE::GLOBAL::$name"; | |||
| 1086 | ||||||
| 1087 | 17 | 10 | my ($orig, $orig_existed); | |||
| 1088 | { | |||||
| 1089 | 26 26 26 17 | 101 49 861 11 | no strict 'refs'; ## no critic (ProhibitNoStrict) | |||
| 1090 | 17 17 | 11 41 | $orig_existed = defined(&{$full}) ? 1 : 0; | |||
| 1091 | # When no prior override existed, push undef rather than \&{$full}. | |||||
| 1092 | # \&{$full} auto-vivifies the stash entry and returns an undef-stub CV. | |||||
| 1093 | # Reinstating that stub on restore would leave a CORE::GLOBAL entry | |||||
| 1094 | # whose non-NULL CV pointer makes Perl treat it as an active override, | |||||
| 1095 | # preventing fallback to the real builtin. _drain_and_restore detects | |||||
| 1096 | # undef and deletes the stash entry instead. | |||||
| 1097 | 17 1 | 18 1 | $orig = $orig_existed ? \&{$full} : undef; | |||
| 1098 | } | |||||
| 1099 | 17 17 | 5 19 | push @{ $mocked{$full} }, $orig; | |||
| 1100 | ||||||
| 1101 | { | |||||
| 1102 | 26 25 25 17 | 73 24 454 13 | no warnings 'redefine', 'prototype'; | |||
| 1103 | 25 25 25 | 41 18 13470 | no strict 'refs'; ## no critic (ProhibitNoStrict) | |||
| 1104 | 17 17 | 12 24 | *{$full} = $wrapper; | |||
| 1105 | } | |||||
| 1106 | ||||||
| 1107 | 17 17 | 14 29 | push @{ $mock_meta{$full} }, { | |||
| 1108 | type => $TYPE // _T_MOCK_CORE, | |||||
| 1109 | installed_at => _caller_info(), | |||||
| 1110 | original_existed => $orig_existed, | |||||
| 1111 | }; | |||||
| 1112 | ||||||
| 1113 | 17 | 22 | return; | |||
| 1114 | } | |||||
| 1115 | ||||||
| 1116 - 1138 | =head2 restore_all
Restore all mocked methods and injected dependencies.
restore_all(); # restore everything
restore_all 'My::Module'; # restore only My::Module's mocks
When called with a package name, the mocks in that package B<and its
sub-packages> are restored: C<restore_all('My::Module')> also restores
C<My::Module::Helper::fn>, but not C<My::ModuleX::fn>. The call-order log is
pruned the same way.
=head3 API SPECIFICATION
=head4 Input
package -- Str, optional
=head4 Output
returns: undef
=cut | |||||
| 1139 | ||||||
| 1140 | sub restore_all { | |||||
| 1141 | 432 | 138550 | my $arg = $_[0]; | |||
| 1142 | ||||||
| 1143 | 432 | 497 | if (defined $arg) { | |||
| 1144 | 14 | 13 | my $package = $arg; | |||
| 1145 | ||||||
| 1146 | 14 | 18 | for my $full_method (keys %mocked) { | |||
| 1147 | 24 | 136 | next unless $full_method =~ /^\Q$package\E::/; | |||
| 1148 | 15 | 22 | _drain_and_restore($full_method); | |||
| 1149 | # _drain_and_restore explicitly skips hash cleanup; do it here | |||||
| 1150 | # to match the behaviour of the global form (%mocked = (); etc.). | |||||
| 1151 | 15 | 13 | delete $mocked{$full_method}; | |||
| 1152 | 15 | 21 | delete $mock_meta{$full_method}; | |||
| 1153 | } | |||||
| 1154 | ||||||
| 1155 | # Remove call_log entries for the restored package | |||||
| 1156 | 14 8 | 18 29 | @call_log = grep { $_ !~ /^\Q$package\E::/ } @call_log; | |||
| 1157 | ||||||
| 1158 | 14 | 16 | return; | |||
| 1159 | } | |||||
| 1160 | ||||||
| 1161 | # Global restore: revert every tracked method to its saved state | |||||
| 1162 | 418 | 594 | _drain_and_restore($_) for keys %mocked; | |||
| 1163 | ||||||
| 1164 | 418 | 465 | %mocked = (); | |||
| 1165 | 418 | 516 | %mock_meta = (); | |||
| 1166 | 418 | 303 | @call_log = (); | |||
| 1167 | ||||||
| 1168 | 418 | 502 | return; | |||
| 1169 | } | |||||
| 1170 | ||||||
| 1171 - 1193 | =head2 restore Restore all mock layers for a single method target. restore 'My::Module::method'; If the method was never mocked this is a no-op. =head3 API SPECIFICATION =head4 Input target -- Str =head4 Output returns: undef =head3 MESSAGES "restore requires a target" -- undef target =cut | |||||
| 1194 | ||||||
| 1195 | sub restore { | |||||
| 1196 | 14 | 9390 | my $target = $_[0]; | |||
| 1197 | ||||||
| 1198 | 14 | 43 | croak 'restore requires a target' unless defined $target; | |||
| 1199 | ||||||
| 1200 | 11 | 13 | my ($package, $method) = _parse_target($target); | |||
| 1201 | 11 | 13 | my $full_method = "${package}::${method}"; | |||
| 1202 | ||||||
| 1203 | 11 | 21 | return unless exists $mocked{$full_method}; | |||
| 1204 | ||||||
| 1205 | 7 | 11 | _drain_and_restore($full_method); | |||
| 1206 | 7 | 6 | delete $mocked{$full_method}; | |||
| 1207 | 7 | 11 | delete $mock_meta{$full_method}; | |||
| 1208 | ||||||
| 1209 | 7 | 7 | return; | |||
| 1210 | } | |||||
| 1211 | ||||||
| 1212 - 1233 | =head2 mock_return Mock a method to always return a fixed value. mock_return 'My::Module::method' => 42; =head3 API SPECIFICATION =head4 Input target -- Str value -- Any =head4 Output returns: undef =head3 MESSAGES "mock_return requires a target and a value" -- target undefined =cut | |||||
| 1234 | ||||||
| 1235 | sub mock_return { | |||||
| 1236 | 39 | 116188 | my ($target, $value) = @_; | |||
| 1237 | ||||||
| 1238 | 39 | 61 | croak 'mock_return requires a target and a value' unless defined $target; | |||
| 1239 | ||||||
| 1240 | 36 | 30 | local $TYPE = _T_MOCK_RETURN; | |||
| 1241 | 36 21 | 59 365 | mock $target => sub { $value }; | |||
| 1242 | ||||||
| 1243 | 36 | 34 | return; | |||
| 1244 | } | |||||
| 1245 | ||||||
| 1246 - 1267 | =head2 mock_exception Mock a method to always throw an exception. mock_exception 'My::Module::method' => 'something went wrong'; =head3 API SPECIFICATION =head4 Input target -- Str message -- Str =head4 Output returns: undef =head3 MESSAGES "mock_exception requires a target and an exception message" -- either missing =cut | |||||
| 1268 | ||||||
| 1269 | sub mock_exception { | |||||
| 1270 | 18 | 12876 | my ($target, $message) = @_; | |||
| 1271 | ||||||
| 1272 | 18 | 64 | croak 'mock_exception requires a target and an exception message' | |||
| 1273 | unless defined $target && defined $message; | |||||
| 1274 | ||||||
| 1275 | 11 | 26 | local $TYPE = _T_MOCK_EXCEPT; | |||
| 1276 | 11 8 | 22 1043 | mock $target => sub { croak $message }; | |||
| 1277 | ||||||
| 1278 | 11 | 9 | return; | |||
| 1279 | } | |||||
| 1280 | ||||||
| 1281 - 1303 | =head2 mock_sequence Mock a method to return a sequence of values over successive calls. The last value repeats when the sequence is exhausted. mock_sequence 'My::Module::method' => (1, 2, 3); =head3 API SPECIFICATION =head4 Input target -- Str values -- Array (one or more) =head4 Output returns: undef =head3 MESSAGES "mock_sequence requires a target and at least one value" -- empty value list =cut | |||||
| 1304 | ||||||
| 1305 | sub mock_sequence { | |||||
| 1306 | 20 | 17896 | my ($target, @values) = @_; | |||
| 1307 | ||||||
| 1308 | 20 | 54 | croak 'mock_sequence requires a target and at least one value' | |||
| 1309 | unless defined $target && @values; | |||||
| 1310 | ||||||
| 1311 | 16 | 16 | my @queue = @values; | |||
| 1312 | ||||||
| 1313 | 16 | 14 | local $TYPE = _T_MOCK_SEQ; | |||
| 1314 | mock $target => sub { | |||||
| 1315 | 30 | 65 | return $queue[0] if @queue == 1; | |||
| 1316 | 8 | 16 | return shift @queue; | |||
| 1317 | 16 | 32 | }; | |||
| 1318 | ||||||
| 1319 | 16 | 19 | return; | |||
| 1320 | } | |||||
| 1321 | ||||||
| 1322 - 1354 | =head2 mock_once
Install a mock that fires exactly once. After the first call the previous
implementation is automatically restored.
mock_once 'My::Module::method' => sub { 'temporary' };
=head3 API SPECIFICATION
=head4 Input
target -- Str
code -- CodeRef
=head4 Output
returns: undef
=head3 MESSAGES
"mock_once requires a target and a coderef" -- missing or non-CODE factory
=head3 PSEUDOCODE
parse target â (package, method)
wrapper = sub {
result = code(@_)
unmock(package, method) -- pop this very layer
return result
}
install wrapper via mock() with TYPE='mock_once'
=cut | |||||
| 1355 | ||||||
| 1356 | sub mock_once { | |||||
| 1357 | 25 | 22471 | my ($target, $code) = @_; | |||
| 1358 | ||||||
| 1359 | 25 | 87 | croak 'mock_once requires a target and a coderef' | |||
| 1360 | unless defined $target && ref($code) eq 'CODE'; | |||||
| 1361 | ||||||
| 1362 | 20 | 22 | my ($package, $method) = _parse_target($target); | |||
| 1363 | ||||||
| 1364 | my $wrapper = sub { | |||||
| 1365 | 16 | 30 | my @result = $code->(@_); | |||
| 1366 | 16 | 34 | Test::Mockingbird::unmock($package, $method); | |||
| 1367 | 16 | 50 | return wantarray ? @result : $result[0]; | |||
| 1368 | 20 | 33 | }; | |||
| 1369 | ||||||
| 1370 | 20 | 20 | local $TYPE = _T_MOCK_ONCE; | |||
| 1371 | 20 | 25 | mock $target => $wrapper; | |||
| 1372 | ||||||
| 1373 | 20 | 23 | return; | |||
| 1374 | } | |||||
| 1375 | ||||||
| 1376 - 1399 | =head2 assert_call_order
Assert that the named methods were called in left-to-right order.
assert_call_order('A::fetch', 'B::process', 'C::save');
Produces one TAP ok/not-ok line and returns a boolean. Intervening calls
to other methods are ignored.
=head3 API SPECIFICATION
=head4 Input
methods -- Array of Str (two or more fully-qualified names)
=head4 Output
returns: Bool
=head3 MESSAGES
"assert_call_order requires at least two method names" -- fewer than two given
=cut | |||||
| 1400 | ||||||
| 1401 | sub assert_call_order { | |||||
| 1402 | 36 | 5031 | my @expected = @_; | |||
| 1403 | ||||||
| 1404 | 36 | 61 | croak 'assert_call_order requires at least two method names' | |||
| 1405 | unless @expected >= 2; | |||||
| 1406 | ||||||
| 1407 | 32 | 21 | my $pos = 0; | |||
| 1408 | 32 | 31 | for my $logged (@call_log) { | |||
| 1409 | 64 | 63 | if ($logged eq $expected[$pos]) { | |||
| 1410 | 52 | 25 | $pos++; | |||
| 1411 | 52 | 51 | last if $pos == @expected; | |||
| 1412 | } | |||||
| 1413 | } | |||||
| 1414 | ||||||
| 1415 | 32 | 27 | my $ok = ($pos == @expected); | |||
| 1416 | ||||||
| 1417 | 32 | 65 | require Test::More; | |||
| 1418 | 32 | 31 | if ($ok) { | |||
| 1419 | 21 | 48 | Test::More::pass("call order: " . join(' -> ', @expected)); | |||
| 1420 | } else { | |||||
| 1421 | 11 | 28 | Test::More::fail("call order: " . join(' -> ', @expected)); | |||
| 1422 | 11 | 6568 | Test::More::diag( | |||
| 1423 | "Expected '$expected[$pos]' next but it was not in the call log" | |||||
| 1424 | ); | |||||
| 1425 | } | |||||
| 1426 | ||||||
| 1427 | 32 | 6674 | return $ok; | |||
| 1428 | } | |||||
| 1429 | ||||||
| 1430 - 1448 | =head2 clear_call_log Clear the call-order log without restoring mocks or spies. clear_call_log(); C<restore_all()> also clears the log automatically. =head3 API SPECIFICATION =head4 Input none =head4 Output returns: undef =cut | |||||
| 1449 | ||||||
| 1450 | sub clear_call_log { | |||||
| 1451 | 7 | 3284 | @call_log = (); | |||
| 1452 | 7 | 7 | return; | |||
| 1453 | } | |||||
| 1454 | ||||||
| 1455 | # _record_call -- Private helper | |||||
| 1456 | # | |||||
| 1457 | # Purpose: Append a fully-qualified method name to the call-order log. | |||||
| 1458 | # Used by Test::Mockingbird::Async to participate in | |||||
| 1459 | # assert_call_order() without crossing the lexical boundary of | |||||
| 1460 | # @call_log. | |||||
| 1461 | # Entry: $_[0] -- Str, fully-qualified method name | |||||
| 1462 | # Exit: undef | |||||
| 1463 | # Side effects: Appends to @call_log | |||||
| 1464 | sub _record_call { | |||||
| 1465 | 17 | 17 | push @call_log, $_[0]; | |||
| 1466 | 17 | 9 | return; | |||
| 1467 | } | |||||
| 1468 | ||||||
| 1469 - 1489 | =head2 diagnose_mocks
Return a structured hashref of all currently active mock layers.
my $diag = diagnose_mocks();
# $diag->{'My::Pkg::method'} = {
# depth => 1,
# layers => [ { type => 'mock_return', installed_at => '...' } ],
# }
=head3 API SPECIFICATION
=head4 Input
none
=head4 Output
returns: HashRef
=cut | |||||
| 1490 | ||||||
| 1491 | sub diagnose_mocks { | |||||
| 1492 | 99 | 8787 | my %report; | |||
| 1493 | ||||||
| 1494 | 99 | 154 | for my $full_method (sort keys %mocked) { | |||
| 1495 | 94 | 102 | my $layers = $mock_meta{$full_method} // []; | |||
| 1496 | $report{$full_method} = { | |||||
| 1497 | 94 | 288 | depth => scalar @{ $mocked{$full_method} }, | |||
| 1498 | layers => [ @$layers ], | |||||
| 1499 | # original_existed reflects whether the method existed before the | |||||
| 1500 | # FIRST mock layer was installed (stored in the bottom-most meta entry) | |||||
| 1501 | 94 | 60 | original_existed => (@$layers && $layers->[0]{original_existed}) ? 1 : 0, | |||
| 1502 | }; | |||||
| 1503 | } | |||||
| 1504 | ||||||
| 1505 | 99 | 111 | return \%report; | |||
| 1506 | } | |||||
| 1507 | ||||||
| 1508 - 1522 | =head2 diagnose_mocks_pretty Return a human-readable multi-line string of all active mock layers. =head3 API SPECIFICATION =head4 Input none =head4 Output returns: Str =cut | |||||
| 1523 | ||||||
| 1524 | sub diagnose_mocks_pretty { | |||||
| 1525 | 9 | 77124 | my $diag = diagnose_mocks(); | |||
| 1526 | 9 | 6 | my @out; | |||
| 1527 | ||||||
| 1528 | 9 | 15 | for my $full_method (sort keys %$diag) { | |||
| 1529 | 12 | 11 | my $entry = $diag->{$full_method}; | |||
| 1530 | 12 | 11 | push @out, "$full_method:"; | |||
| 1531 | 12 | 14 | push @out, " depth: $entry->{depth}"; | |||
| 1532 | 12 | 14 | push @out, " original_existed: $entry->{original_existed}"; | |||
| 1533 | 12 12 | 5 17 | for my $layer (@{ $entry->{layers} }) { | |||
| 1534 | push @out, sprintf " - type: %-14s installed_at: %s", | |||||
| 1535 | 12 | 26 | $layer->{type}, $layer->{installed_at}; | |||
| 1536 | } | |||||
| 1537 | 12 | 10 | push @out, ''; | |||
| 1538 | } | |||||
| 1539 | ||||||
| 1540 | 9 | 26 | return join "\n", @out; | |||
| 1541 | } | |||||
| 1542 | ||||||
| 1543 | # _drain_and_restore -- Private helper | |||||
| 1544 | # | |||||
| 1545 | # Purpose: Pop all layers from the mock stack for a single target and | |||||
| 1546 | # restore the bottom-most saved coderef to the symbol table. | |||||
| 1547 | # Does NOT clean up %mocked or %mock_meta -- callers must do | |||||
| 1548 | # that themselves. | |||||
| 1549 | # Entry: $_[0] -- Str, fully-qualified method name | |||||
| 1550 | # Exit: undef | |||||
| 1551 | # Side effects: Modifies the symbol table for the target. | |||||
| 1552 | sub _drain_and_restore { | |||||
| 1553 | 450 | 298 | my $full_method = $_[0]; | |||
| 1554 | ||||||
| 1555 | 450 | 255 | my $final_prev; | |||
| 1556 | 450 994 | 275 853 | while (@{ $mocked{$full_method} }) { | |||
| 1557 | 544 544 | 314 496 | $final_prev = pop @{ $mocked{$full_method} }; | |||
| 1558 | } | |||||
| 1559 | ||||||
| 1560 | 450 | 483 | _restore_slot($full_method, $final_prev); | |||
| 1561 | ||||||
| 1562 | 450 | 397 | return; | |||
| 1563 | } | |||||
| 1564 | ||||||
| 1565 | # _capture_slot -- Private helper | |||||
| 1566 | # | |||||
| 1567 | # Purpose: Snapshot a target's CODE slot before a layer is installed. | |||||
| 1568 | # Entry: $_[0] -- Str, fully-qualified sub name | |||||
| 1569 | # Exit: ($existed, $saved) -- $existed is 1 if a sub with a body is | |||||
| 1570 | # installed, else 0. $saved is a reference to the current | |||||
| 1571 | # CODE slot, or undef if no sub of that name has been declared | |||||
| 1572 | # at all. In the undef case \&{...} is deliberately NOT taken: | |||||
| 1573 | # it would auto-vivify an empty stub, and restoring that stub | |||||
| 1574 | # would shadow any inherited method of the same name. | |||||
| 1575 | # Side effects: None. | |||||
| 1576 | sub _capture_slot { | |||||
| 1577 | 657 | 423 | my $full = $_[0]; | |||
| 1578 | ||||||
| 1579 | 25 25 25 | 70 21 1160 | no strict 'refs'; ## no critic (ProhibitNoStrict) | |||
| 1580 | 657 657 | 345 1013 | my $existed = defined(&{$full}) ? 1 : 0; | |||
| 1581 | 657 657 616 | 405 691 536 | my $saved = exists(&{$full}) ? \&{$full} : undef; | |||
| 1582 | ||||||
| 1583 | 657 | 653 | return ($existed, $saved); | |||
| 1584 | } | |||||
| 1585 | ||||||
| 1586 | # _restore_slot -- Private helper | |||||
| 1587 | # | |||||
| 1588 | # Purpose: Put a saved CODE slot back (the inverse of _capture_slot). | |||||
| 1589 | # Entry: $_[0] -- Str, fully-qualified sub name | |||||
| 1590 | # $_[1] -- CodeRef saved by _capture_slot, or undef | |||||
| 1591 | # Exit: undef | |||||
| 1592 | # Side effects: Modifies the symbol table for the target. The GV itself is | |||||
| 1593 | # never deleted (except under CORE::GLOBAL, below) because | |||||
| 1594 | # compiled direct-call ops cache the GV at compile time; a new | |||||
| 1595 | # GV would be invisible to them. | |||||
| 1596 | sub _restore_slot { | |||||
| 1597 | 578 | 464 | my ($full, $prev) = @_; | |||
| 1598 | ||||||
| 1599 | 25 25 25 | 47 20 365 | no strict 'refs'; ## no critic (ProhibitNoStrict) | |||
| 1600 | ||||||
| 1601 | 578 | 532 | if (defined $prev) { | |||
| 1602 | 25 25 25 | 37 22 3193 | no warnings 'redefine', 'prototype'; | |||
| 1603 | 523 523 | 281 1444 | *{$full} = $prev; | |||
| 1604 | } elsif ($full =~ /^CORE::GLOBAL::([^:]+)$/) { | |||||
| 1605 | # No prior CORE::GLOBAL override existed (mock_core pushed undef). | |||||
| 1606 | # Delete the stash entry entirely so Perl's builtin lookup falls back | |||||
| 1607 | # to the real CORE function. Reinstating an undef-stub CV is not | |||||
| 1608 | # sufficient: Perl treats any non-NULL CV in CORE::GLOBAL as an active | |||||
| 1609 | # user override, so the real builtin would never be reached. | |||||
| 1610 | 16 | 22 | delete $CORE::GLOBAL::{$1}; | |||
| 1611 | } else { | |||||
| 1612 | # No sub was declared before the first layer went on. Empty the CODE | |||||
| 1613 | # slot while keeping the GV and its other slots, so that method lookup | |||||
| 1614 | # falls through to @ISA again and a direct call dies with | |||||
| 1615 | # "Undefined subroutine". Assigning undef to a glob empties every | |||||
| 1616 | # slot, so the non-CODE ones are saved and put back. | |||||
| 1617 | 39 | 88 | my ($package) = $full =~ /^(.*)::[^:]+$/; | |||
| 1618 | 39 39 | 26 70 | my $gv = \*{$full}; | |||
| 1619 | 39 39 | 20 64 | my %keep = map { $_ => *{$gv}{$_} } | |||
| 1620 | 39 195 195 | 48 103 174 | grep { defined *{$gv}{$_} } qw(SCALAR ARRAY HASH IO FORMAT); | |||
| 1621 | 39 39 | 29 67 | undef *{$gv}; | |||
| 1622 | 39 39 | 123 45 | *{$gv} = $keep{$_} for keys %keep; | |||
| 1623 | 39 | 65 | mro::method_changed_in($package); | |||
| 1624 | } | |||||
| 1625 | ||||||
| 1626 | 578 | 491 | return; | |||
| 1627 | } | |||||
| 1628 | ||||||
| 1629 | # _call_through -- Private helper | |||||
| 1630 | # | |||||
| 1631 | # Purpose: Return the coderef a call-through wrapper (spy, before, | |||||
| 1632 | # after, around, async_spy) should delegate to. | |||||
| 1633 | # Entry: $_[0] -- Str, package; $_[1] -- Str, method | |||||
| 1634 | # Exit: CodeRef. If a sub of that name is declared in the package it | |||||
| 1635 | # is returned directly. Otherwise a delegator is returned that, | |||||
| 1636 | # at call time, finds the method in the package's parent classes | |||||
| 1637 | # (as Perl's own method lookup would once the wrapper is | |||||
| 1638 | # removed) and croaks "Undefined subroutine" if there is none. | |||||
| 1639 | # Taking \&{...} here instead would return a stub that re-enters | |||||
| 1640 | # the wrapper through the GV and loops forever. | |||||
| 1641 | # Side effects: None. | |||||
| 1642 | sub _call_through { | |||||
| 1643 | 239 | 201 | my ($package, $method) = @_; | |||
| 1644 | 239 | 158 | my $full = "${package}::${method}"; | |||
| 1645 | ||||||
| 1646 | { | |||||
| 1647 | 25 25 25 239 | 65 25 1850 134 | no strict 'refs'; ## no critic (ProhibitNoStrict) | |||
| 1648 | 239 223 239 | 122 278 315 | return \&{$full} if exists &{$full}; | |||
| 1649 | } | |||||
| 1650 | ||||||
| 1651 | return sub { | |||||
| 1652 | 11 | 13 | my $code = _inherited_method($package, $method) | |||
| 1653 | or croak "Undefined subroutine &$full called"; | |||||
| 1654 | 8 | 13 | goto &$code; | |||
| 1655 | 16 | 33 | }; | |||
| 1656 | } | |||||
| 1657 | ||||||
| 1658 | # _inherited_method -- Private helper | |||||
| 1659 | # | |||||
| 1660 | # Purpose: Find a method in a package's ancestors, skipping the package | |||||
| 1661 | # itself. | |||||
| 1662 | # Entry: $_[0] -- Str, package; $_[1] -- Str, method | |||||
| 1663 | # Exit: CodeRef of the first defined ancestor implementation in | |||||
| 1664 | # method resolution order (UNIVERSAL last), or undef if none. | |||||
| 1665 | # Side effects: None. | |||||
| 1666 | sub _inherited_method { | |||||
| 1667 | 11 | 26 | my ($package, $method) = @_; | |||
| 1668 | ||||||
| 1669 | 11 11 | 7 22 | my (undef, @ancestors) = @{ mro::get_linear_isa($package) }; | |||
| 1670 | 11 | 16 | push @ancestors, 'UNIVERSAL' unless $package eq 'UNIVERSAL'; | |||
| 1671 | ||||||
| 1672 | 25 25 25 | 53 14 11576 | no strict 'refs'; ## no critic (ProhibitNoStrict) | |||
| 1673 | 11 | 9 | for my $class (@ancestors) { | |||
| 1674 | 10 8 10 | 6 16 21 | return \&{"${class}::${method}"} if defined &{"${class}::${method}"}; | |||
| 1675 | } | |||||
| 1676 | ||||||
| 1677 | 3 | 19 | return; | |||
| 1678 | } | |||||
| 1679 | ||||||
| 1680 | # _parse_target -- Private helper | |||||
| 1681 | # | |||||
| 1682 | # Purpose: Normalise both shorthand ('Pkg::method') and longhand | |||||
| 1683 | # ('Pkg', 'method') call forms into a ($package, $method) pair. | |||||
| 1684 | # Entry: @_ -- one arg for shorthand, two args for longhand | |||||
| 1685 | # Exit: ($package, $method) -- list of two strings | |||||
| 1686 | sub _parse_target { | |||||
| 1687 | 270 | 86702 | my ($arg1, $arg2) = @_; | |||
| 1688 | ||||||
| 1689 | # Shorthand: single 'Pkg::method' string -- arg2 is absent (undef) | |||||
| 1690 | 270 | 893 | if (defined $arg1 && !defined $arg2 && $arg1 =~ /^(.*)::([^:]+)$/) { | |||
| 1691 | 219 | 398 | return ($1, $2); | |||
| 1692 | } | |||||
| 1693 | ||||||
| 1694 | 51 | 75 | return ($arg1, $arg2); | |||
| 1695 | } | |||||
| 1696 | ||||||
| 1697 | # _caller_info -- Private helper | |||||
| 1698 | # | |||||
| 1699 | # Purpose: Walk up the call stack to find the first frame outside any | |||||
| 1700 | # Test::Mockingbird namespace. Returns a human-readable | |||||
| 1701 | # "file line N" string for use in installed_at diagnostics. | |||||
| 1702 | # This ensures that sugar functions (mock_return, mock_once, | |||||
| 1703 | # etc.) report the user's call site, not their own location. | |||||
| 1704 | # Entry: none | |||||
| 1705 | # Exit: Str, e.g. "t/my_test.t line 42" | |||||
| 1706 | sub _caller_info { | |||||
| 1707 | 675 | 2219 | my $level = 1; | |||
| 1708 | 675 | 710 | while (my @info = caller($level)) { | |||
| 1709 | 1075 | 7527 | last unless $info[0] =~ /^Test::Mockingbird/; | |||
| 1710 | 400 | 496 | $level++; | |||
| 1711 | } | |||||
| 1712 | 675 | 727 | my @info = caller($level); | |||
| 1713 | 675 | 4699 | return defined $info[1] ? "$info[1] line $info[2]" : '(unknown)'; | |||
| 1714 | } | |||||
| 1715 | ||||||
| 1716 | # _get_prototype -- Private helper | |||||
| 1717 | # | |||||
| 1718 | # Return the prototype string of a named sub, if any. | |||||
| 1719 | # | |||||
| 1720 | # Entry: $_[0] -- Str, fully-qualified sub name | |||||
| 1721 | # Exit: Str or undef | |||||
| 1722 | sub _get_prototype { | |||||
| 1723 | 11 | 10230 | my $full = $_[0]; | |||
| 1724 | ||||||
| 1725 | # All components (package segments and the sub name itself) must start | |||||
| 1726 | # with a letter or underscore -- Perl identifiers cannot begin with a digit. | |||||
| 1727 | 11 | 74 | croak "Invalid fully-qualified name '$full'" | |||
| 1728 | unless $full =~ /^[A-Za-z_]\w*(?:::[A-Za-z_]\w*)+$/; | |||||
| 1729 | ||||||
| 1730 | 6 | 17 | my ($pkg, $sub) = $full =~ /^(.*)::([^:]+)$/; | |||
| 1731 | 6 | 35 | my $code = $pkg->can($sub) or return; | |||
| 1732 | 4 | 9 | return prototype($code); | |||
| 1733 | } | |||||
| 1734 | ||||||
| 1735 | # _core_delegator -- Private helper | |||||
| 1736 | # | |||||
| 1737 | # Purpose: Build the $call_core coderef that mock_core() passes to the | |||||
| 1738 | # replacement, calling the real CORE::$name directly (which | |||||
| 1739 | # bypasses any CORE::GLOBAL override). It must be string-eval'd | |||||
| 1740 | # because CORE:: names are compile-time constructs with no | |||||
| 1741 | # runtime coderef. | |||||
| 1742 | # Entry: $_[0] -- Str, builtin name; $_[1] -- Str|undef, its prototype | |||||
| 1743 | # Exit: CodeRef. When the prototype is a fixed list of scalar-like | |||||
| 1744 | # slots (e.g. '' for time, '$$;$' for index, '*\$$;$' for read) | |||||
| 1745 | # the arguments are passed by position, choosing the call by | |||||
| 1746 | # argument count, since CORE::time(@_) or CORE::index(@_) does | |||||
| 1747 | # not compile. A lone '_' passes $_[0], avoiding the "Array | |||||
| 1748 | # passed to stat will be coerced to a scalar" warning. Other | |||||
| 1749 | # prototypes pass @_. If no call compiles, the delegator | |||||
| 1750 | # croaks when invoked, so the builtin can still be mocked as | |||||
| 1751 | # long as the replacement does not call through. | |||||
| 1752 | # Side effects: None. | |||||
| 1753 | sub _core_delegator { | |||||
| 1754 | 17 | 17 | my ($name, $proto) = @_; | |||
| 1755 | ||||||
| 1756 | 17 | 8 | my $code; | |||
| 1757 | 17 | 61 | if (defined $proto && $proto eq '_') { | |||
| 1758 | 1 | 1 | $code = "CORE::$name(\$_[0])"; | |||
| 1759 | } elsif (defined $proto && $proto =~ /^((?:\\?[\$_*])*);?((?:\\?[\$_*])*)$/) { | |||||
| 1760 | # Required slots, then optional ones after the ';' (if any) | |||||
| 1761 | 6 | 10 | my ($req, $opt) = ($1, $2); | |||
| 1762 | 6 | 7 | my $min = () = $req =~ /[\$_*]/g; | |||
| 1763 | 6 | 9 | my $max = $min + (() = $opt =~ /[\$_*]/g); | |||
| 1764 | my @calls = map { | |||||
| 1765 | 6 11 | 11 7 | my $n = $_; | |||
| 1766 | 11 16 | 14 20 | "CORE::$name(" . join(', ', map { "\$_[$_]" } 0 .. $n - 1) . ')'; | |||
| 1767 | } $min .. $max; | |||||
| 1768 | # Most arguments first: '@_ >= 3 ? f(a,b,c) : @_ >= 2 ? f(a,b) : f(a)' | |||||
| 1769 | 6 | 7 | $code = pop @calls; | |||
| 1770 | 6 | 7 | for my $n (reverse $min .. $max - 1) { | |||
| 1771 | 5 | 10 | $code = "\@_ > $n ? $code : " . pop @calls; | |||
| 1772 | } | |||||
| 1773 | } else { | |||||
| 1774 | 10 | 7 | $code = "CORE::$name(\@_)"; | |||
| 1775 | } | |||||
| 1776 | ||||||
| 1777 | 17 2 2 2 | 668 6 2 59 | my $sub = eval "sub { no warnings 'syntax'; $code }"; ## no critic (ProhibitStringyEval) | |||
| 1778 | 17 | 36 | return $sub if $sub; | |||
| 1779 | ||||||
| 1780 | 1 | 1 | my $err = $@; | |||
| 1781 | 1 1 | 2 314 | return sub { croak "mock_core: cannot call through to CORE::$name: $err" }; | |||
| 1782 | } | |||||
| 1783 | ||||||
| 1784 | # _is_name -- Private helper | |||||
| 1785 | # | |||||
| 1786 | # Purpose: True if the argument can name a package or sub: defined and | |||||
| 1787 | # non-empty. Plain truthiness would reject the valid name '0'. | |||||
| 1788 | # Entry: $_[0] -- Any | |||||
| 1789 | # Exit: Bool | |||||
| 1790 | sub _is_name { | |||||
| 1791 | 1907 | 3492 | return defined $_[0] && length $_[0]; | |||
| 1792 | } | |||||
| 1793 | ||||||
| 1794 | # _is_core_overridable -- Private helper | |||||
| 1795 | # | |||||
| 1796 | # Determine whether a bare name refers to a CORE builtin that | |||||
| 1797 | # Perl allows packages to shadow with a user sub. | |||||
| 1798 | # Entry: $_[0] -- Str, simple identifier (no 'CORE::' prefix) | |||||
| 1799 | # Exit: Bool -- true if prototype("CORE::$name") succeeds (even if | |||||
| 1800 | # the returned prototype is undef, meaning "no prototype") | |||||
| 1801 | sub _is_core_overridable { | |||||
| 1802 | 18 | 10 | my $name = $_[0]; | |||
| 1803 | 18 | 10 | local $@; | |||
| 1804 | 18 18 17 | 15 55 14 | eval { my $p = prototype("CORE::$name"); 1 }; | |||
| 1805 | 18 | 28 | return !$@; | |||
| 1806 | } | |||||
| 1807 | ||||||
| 1808 - 2037 | =head1 SUPPORT
This module is provided as-is without any warranty.
Please report bugs at L<https://github.com/nigelhorne/Test-Mockingbird/issues>.
=head1 AUTHOR
Nigel Horne, C<< <njh at nigelhorne.com> >>
=head1 SEE ALSO
=over 4
=item * L<Test Dashboard|https://nigelhorne.github.io/Test-Mockingbird/coverage/>
=item * L<Test::Mockingbird::Async>
=item * L<Test::Mockingbird::DeepMock>
=item * L<Test::Mockingbird::TimeTravel>
=back
=head1 REPOSITORY
L<https://github.com/nigelhorne/Test-Mockingbird>
=head1 FORMAL SPECIFICATION
Notation: C<mocked[t]> is the saved-slot stack for target C<t> (head = most
recent), C<sym[t].CODE> its CODE slot, C<declared(t)> whether a sub of that
name exists in the package (C<exists &t>), and C<name(x)> means
C<defined(x) â§ x â ''>. C<saved(t)> is C<sym[t].CODE> if C<declared(t)>,
else C<undef>. C<resolve(p, m)> is C<sym[p::m].CODE> if C<declared(p::m)>,
else a delegator that at call time invokes the first defined C<a::m> for
C<a> in C<tail(linear_isa(p)) ⢠â¨UNIVERSALâ©>, or croaks C<"Undefined
subroutine &p::m called">.
=head2 mock
mock â
â pkg, meth : Str; replacement : CodeRef â¢
pre name(pkg) â§ name(meth) â§ ref(replacement) = 'CODE'
let t = pkg::meth â¢
post mocked'[t] = â¨saved(t)⩠⢠mocked[t]
â§ sym'[t].CODE = replacement
â§ (saved(t) â undef â prototype(replacement) = prototype(saved(t)))
â§ mock_meta'[t] = â¨{ type, installed_at, original_existed }⩠⢠mock_meta[t]
=head2 unmock
unmock â
â t : Str â¢
mocked[t] = â¨â© â no change
mocked[t] â â¨â© â
let prev = head(mocked[t]) â¢
post mocked'[t] = tail(mocked[t]) â§ mock_meta'[t] = tail(mock_meta[t])
â§ restore_slot(t, prev)
restore_slot(t, prev) â
prev â undef â sym'[t].CODE = prev
prev = undef â§ t â CORE::GLOBAL â t â dom(stash')
prev = undef â§ t â CORE::GLOBAL â sym'[t].CODE = â
â§ sym'[t] = sym[t] on every other slot
â§ t â dom(stash')
=head2 before
before â
â pkg, meth : Str; hook : CodeRef â¢
pre name(pkg) â§ name(meth) â§ ref(hook) = 'CODE'
let orig = resolve(pkg, meth) â¢
post mock(pkg::meth, wrapper)
â§ wrapper(@args) â hook(@args); orig(@args) -- caller's context
=head2 after
after â
â pkg, meth : Str; hook : CodeRef â¢
pre name(pkg) â§ name(meth) â§ ref(hook) = 'CODE'
let orig = resolve(pkg, meth) â¢
post mock(pkg::meth, wrapper)
â§ wrapper(@args) â let ret = orig(@args) ⢠hook(@args); ret
â§ orig dies â hook not called
=head2 around
around â
â pkg, meth : Str; hook : CodeRef â¢
pre name(pkg) â§ name(meth) â§ ref(hook) = 'CODE'
let orig = resolve(pkg, meth) â¢
post mock(pkg::meth, wrapper) â§ wrapper(@args) â hook(orig, @args)
=head2 mock_scoped
mock_scoped â
install all mocks via mock()
â§ return Guard(full_methods)
â§ Guard.DESTROY â â m â full_methods ⢠unmock(m)
=head2 spy
spy â
â pkg, meth : Str â¢
pre name(pkg) â§ name(meth)
let t = pkg::meth; orig = resolve(pkg, meth) â¢
post mocked'[t] = â¨saved(t)⩠⢠mocked[t]
â§ sym'[t].CODE = wrapper â§ prototype(wrapper) = prototype(orig)
â§ wrapper(@args) â calls' = calls ⢠â¨[t, @args]â©
â§ call_log' = call_log ⢠â¨tâ©
â§ orig(@args)
â§ returns sub { calls }
=head2 inject
inject â
â pkg, dep : Str; val : Any â¢
pre name(pkg) â§ name(dep)
let t = pkg::dep â¢
post mocked'[t] = â¨saved(t)⩠⢠mocked[t] â§ sym'[t].CODE = sub { val }
=head2 inject_all
inject_all â
â pkg : Str; deps : HashRef â¢
post â (k,v) â deps ⢠inject(pkg, k, v)
=head2 intercept_new
intercept_new â
â class : Str; factory : Any â¢
pre class â '' â§ @args ⥠2
let rep = (factory : CodeRef) ? factory : sub { factory } â¢
post mock("${class}::new", rep)
=head2 mock_core
mock_core â
â name : Str; replacement : CodeRef â¢
pre _is_core_overridable(name) â§ ref(replacement) = 'CODE'
let t = CORE::GLOBAL::name â¢
let call_core = core_delegator(name, prototype(CORE::name)) â¢
let wrapper = sub { replacement(call_core, @_) } â¢
post sym'[t].CODE = wrapper
â§ prototype(wrapper) = prototype(CORE::name) // '@'
â§ mocked'[t] = â¨defined(sym[t].CODE) ? sym[t].CODE : undef⩠⢠mocked[t]
core_delegator(name, proto) â
call_core(@a) = CORE::name(@a) with @a passed by position for a
fixed-slot proto, as $a[0] for '_', as @a otherwise; if no such call
compiles, call_core croaks "mock_core: cannot call through to ...".
=head2 restore_all
restore_all â
global: â t â dom(mocked) ⢠restore_slot(t, last(mocked[t]))
â§ mocked' = {} â§ mock_meta' = {} â§ call_log' = []
scoped: â t â dom(mocked) ⢠t =~ /^\Qpkg\E::/ â
restore_slot(t, last(mocked[t])) â§ t â dom(mocked') ⪠dom(mock_meta')
â§ call_log' = [ e â call_log | e !~ /^\Qpkg\E::/ ]
-- sub-packages (pkg::Inner::m) match; pkgX::m does not
=head2 restore
restore â
â t : Str â¢
pre defined(t)
post t â dom(mocked) â restore_slot(t, last(mocked[t]))
â§ t â dom(mocked') ⪠dom(mock_meta')
=head2 mock_return
mock_return â
â target : Str; value : Any â¢
post sym_table'[target].CODE = sub { value }
=head2 mock_exception
mock_exception â
â target : Str; msg : Str â¢
post sym_table'[target].CODE = sub { croak msg }
=head2 mock_sequence
mock_sequence â
â target : Str; values : Seq(Any) â¢
pre |values| ⥠1
post let queue = values â¢
sym_table'[target].CODE = sub { head(queue) if |queue|=1 else shift(queue) }
=head2 mock_once
mock_once â
â target : Str; code : CodeRef â¢
post sym_table'[target] = sub {
result = code(@args)
unmock(target)
return result
}
=head2 assert_call_order
assert_call_order â
â expected : Seq(Str) â¢
pre |expected| ⥠2
post result = (â i ⢠â p_i : â | p_0 < p_1 < ⦠⧠call_log[p_i] = expected[i])
=head2 clear_call_log
clear_call_log â post call_log' = []
=head2 diagnose_mocks
diagnose_mocks â
returns { target ⦠{ depth, original_existed, layers } | target â dom(mocked) }
=head2 diagnose_mocks_pretty
diagnose_mocks_pretty â stringify(diagnose_mocks())
=head1 LICENCE AND COPYRIGHT
Copyright 2025-2026 Nigel Horne.
Usage is subject to the GPL2 licence terms.
If you use it,
please let me know.
=cut | |||||
| 2038 | ||||||
| 2039 | 1; | |||||
| 2040 | ||||||
| 2041 | package Test::Mockingbird::Guard; | |||||
| 2042 | ||||||
| 2043 | # Guard object returned by mock_scoped. Stores a list of fully-qualified | |||||
| 2044 | # method names and calls unmock() on each when destroyed. | |||||
| 2045 | ||||||
| 2046 | sub new { | |||||
| 2047 | 28 | 35 | my ($class, @full_methods) = @_; | |||
| 2048 | 28 | 67 | return bless { full_methods => \@full_methods }, $class; | |||
| 2049 | } | |||||
| 2050 | ||||||
| 2051 | sub DESTROY { | |||||
| 2052 | 28 | 14064 | my $self = $_[0]; | |||
| 2053 | 28 28 | 23 59 | Test::Mockingbird::unmock($_) for @{ $self->{full_methods} }; | |||
| 2054 | 28 | 44 | return; | |||
| 2055 | } | |||||
| 2056 | ||||||
| 2057 | 1; | |||||