File Coverage

File:blib/lib/Test/Mockingbird.pm
Coverage:99.5%

linestmtbrancondsubtimecode
1package 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.
92use 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
108our @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.
135our $TYPE;
136
137# Internal mocking state -- module-level lexicals.
138my %mocked;    # full_method => [ stack of saved coderefs; undef = no sub was declared ]
139my %mock_meta; # full_method => [ { type => ..., installed_at => ... }, ... ]
140my @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
156our $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
322sub 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
411sub 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
478sub 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
548sub 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
631sub 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
693sub 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
771sub 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
848sub 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
921sub 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
968sub 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
1056sub 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
1140sub 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
1195sub 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
1235sub 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
1269sub 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
1305sub 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
1356sub 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
1401sub 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
1450sub 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
1464sub _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
1491sub 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
1524sub 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.
1552sub _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.
1576sub _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.
1596sub _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.
1642sub _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.
1666sub _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
1686sub _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"
1706sub _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
1722sub _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.
1753sub _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
1790sub _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")
1801sub _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
20391;
2040
2041package 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
2046sub new {
2047
28
35
        my ($class, @full_methods) = @_;
2048
28
67
        return bless { full_methods => \@full_methods }, $class;
2049}
2050
2051sub 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
20571;