File Coverage

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

linestmtbrancondsubtimecode
1package Test::Mockingbird;
2
3
23
23
23
1539335
17
315
use strict;
4
23
23
23
35
13
390
use warnings;
5
23
23
174
39
use 5.016003;
6
7# TODO: Add $ENV{TEST_MOCKINGBIRD_DEBUG} tracing flag that emits one line to
8#       STDERR on each mock/unmock/restore_all operation (name, type, caller
9#       location).  diagnose_mocks() covers post-hoc inspection; this would add
10#       real-time per-operation tracing for troubleshooting complex stacking
11#       scenarios.  Inspired by Function::Override's PERL_FUNCTION_OVERRIDE_DEBUG.
12
13
23
23
23
40
24
500
use Carp       qw(croak carp);
14
23
23
23
47
21
223
use Exporter   'import';
15
23
23
23
33
21
489
use Scalar::Util ();
16
17# Internal type-name constants -- eliminate magic strings.
18# These constants are used wherever a layer type is recorded in %mock_meta.
19use constant {
20
23
3555
        _T_MOCK          => 'mock',
21        _T_SPY           => 'spy',
22        _T_INJECT        => 'inject',
23        _T_MOCK_RETURN   => 'mock_return',
24        _T_MOCK_EXCEPT   => 'mock_exception',
25        _T_MOCK_SEQ      => 'mock_sequence',
26        _T_MOCK_ONCE     => 'mock_once',
27        _T_MOCK_SCOPED   => 'mock_scoped',
28        _T_INTERCEPT_NEW => 'intercept_new',
29        _T_BEFORE        => 'before',
30        _T_AFTER         => 'after',
31        _T_AROUND        => 'around',
32        _T_MOCK_CORE     => 'mock_core',
33
23
23
31
18
};
34
35our @EXPORT = qw(
36        mock
37        unmock
38        mock_scoped
39        mock_core
40        before
41        after
42        around
43        spy
44        inject
45        inject_all
46        intercept_new
47        restore
48        restore_all
49        mock_return
50        mock_exception
51        mock_sequence
52        mock_once
53        diagnose_mocks
54        diagnose_mocks_pretty
55        assert_call_order
56        clear_call_log
57);
58
59# $TYPE is set via 'local' by sugar functions before delegating to mock()
60# or inject() so that diagnose_mocks() records the correct layer type.
61# External modules (e.g. Test::Mockingbird::Async) use the same mechanism.
62our $TYPE;
63
64# Internal mocking state -- module-level lexicals.
65my %mocked;    # full_method => [ stack of coderefs (or undef stubs) ]
66my %mock_meta; # full_method => [ { type => ..., installed_at => ... }, ... ]
67my @call_log;  # ordered log of every spied call
68
69 - 79
=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.13

=cut
80
81our $VERSION = '0.13';
82
83 - 237
=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

=head2 diagnose_mocks

Returns a structured hashref of all active mock layers.

=head2 diagnose_mocks_pretty

Returns a human-readable multi-line string of all active mock layers.

=head2 Diagnostic Metadata

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 C<< ->can() >> may return truthy after unmocking a never-existed method

Perl's typeglob (GV) system auto-vivifies a GV entry the first time
C<\&{$full_method}> is called internally (in C<mock()>, C<spy()>, or
C<inject()>). After unmocking, this GV entry remains in the stash with an
"undefined sub" placeholder in the CODE slot. C<< Package->can('method') >>
tests the GV's existence in the stash, not whether the CODE slot is defined,
so it may still return a truthy value.

To test whether a sub is callable, use C<defined(&Package::method)> rather
than C<< Package->can('method') >>. C<defined(&...)> correctly returns false
for the placeholder stub. Calling the stub dies with C<"Undefined subroutine">.

Deleting the GV from the stash (via C<delete $stash{method}>) would make
C<< ->can() >> return false but would break subsequent mock/inject stacking:
compiled direct calls (C<Package::method()>) cache the GV at compile time,
so a new GV installed after a delete is invisible to those compiled calls.

=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

=encoding utf-8

=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 the auto-vivified undef stub if the method does not exist) and installs
the replacement. C<unmock()> pops one layer; C<restore_all()> drains all.

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" -- target or coderef missing

=cut
238
239sub mock {
240
412
538929
        my ($arg1, $arg2, $arg3) = @_;
241
242
412
267
        my ($package, $method, $replacement);
243
244        # Shorthand: 'Pkg::method' => $code (arg3 absent)
245
412
1068
        if (defined $arg1 && !defined $arg3 && $arg1 =~ /^(.*)::([^:]+)$/) {
246
242
297
                ($package, $method, $replacement) = ($1, $2, $arg2);
247        } else {
248
170
152
                ($package, $method, $replacement) = ($arg1, $arg2, $arg3);
249        }
250
251
412
794
        croak 'Package, method and replacement are required for mocking'
252                unless $package && $method && $replacement;
253
254
395
249
        my $full_method = "${package}::${method}";
255
256        # Capture the current CODE slot (or the undef-stub if the method does
257        # not yet exist).  We always capture via \& so that on restore we
258        # write back to the SAME GV that compiled direct calls hold, rather
259        # than deleting the GV and creating a new one that compiled ops miss.
260
395
234
        my ($original, $orig_existed);
261        {
262
23
23
23
395
81
31
1183
206
                no strict 'refs';    ## no critic (ProhibitNoStrict)
263
395
395
212
583
                $orig_existed = defined(&{$full_method}) ? 1 : 0;
264
395
395
237
398
                $original     = \&{$full_method};
265        }
266
395
395
305
389
        push @{ $mocked{$full_method} }, $original;
267
268        # Stamp the prototype onto the replacement to avoid Perl warning about
269        # "Prototype mismatch" when the original had a prototype.
270
395
299
        my $orig_proto = prototype($original);
271
395
299
        if (defined $orig_proto) {
272
16
35
                &Scalar::Util::set_prototype($replacement, $orig_proto);
273        }
274
275        {
276                # 'redefine' suppresses "Subroutine ... redefined".
277                # 'prototype' suppresses "Prototype mismatch" -- that warning lives in
278                # a separate category and is not covered by 'redefine'.  set_prototype()
279                # above should already make the prototypes equal, but on some Perl builds
280                # the GV-level check still fires before the CV slot is fully updated, so
281                # we suppress the warning here.
282
23
23
23
395
48
15
405
209
                no warnings 'redefine', 'prototype';
283
23
23
23
35
20
2675
                no strict 'refs';    ## no critic (ProhibitNoStrict)
284
395
395
190
535
                *{$full_method} = $replacement;
285        }
286
287
395
395
244
549
        push @{ $mock_meta{$full_method} }, {
288                type             => $TYPE // _T_MOCK,
289                installed_at     => _caller_info(),
290                original_existed => $orig_existed,
291        };
292
293
395
370
        return;
294}
295
296 - 322
=head2 unmock

Restore the previous implementation of a mocked method (one layer).

    unmock('My::Module', 'method');
    unmock 'My::Module::method';

If the method did not exist before it was mocked, the original undef-stub
is restored so that calling the method dies with C<"Undefined subroutine">.
Note: C<< ->can() >> may still return truthy; use C<defined(&...)> to test
whether a method is callable. See L</LIMITATIONS>.

=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
323
324sub unmock {
325
106
19035
        my ($arg1, $arg2) = @_;
326
327
106
83
        my ($package, $method);
328
106
371
        if (defined $arg1 && !defined $arg2 && $arg1 =~ /^(.*)::([^:]+)$/) {
329
79
103
                ($package, $method) = ($1, $2);
330        } else {
331
27
23
                ($package, $method) = ($arg1, $arg2);
332        }
333
334
106
188
        croak 'Package and method are required for unmocking'
335                unless $package && $method;
336
337
100
85
        my $full_method = "${package}::${method}";
338
339        # Nothing to do if this method was never mocked
340
100
95
120
122
        return unless exists $mocked{$full_method} && @{ $mocked{$full_method} };
341
342
95
95
69
96
        my $prev = pop @{ $mocked{$full_method} };
343
344
95
98
        if (defined $prev) {
345
23
23
23
57
19
415
                no warnings 'redefine';
346
23
23
23
38
18
1079
                no strict 'refs';    ## no critic (ProhibitNoStrict)
347
94
94
62
208
                *{$full_method} = $prev;
348        } elsif ($full_method =~ /^CORE::GLOBAL::/) {
349                # No prior CORE::GLOBAL override existed (mock_core pushed undef).
350                # Delete the stash entry so the real CORE builtin is reachable.
351
1
4
                my ($bname) = ($full_method =~ /::([^:]+)$/);
352
23
23
23
68
16
2545
                no strict 'refs';    ## no critic (ProhibitNoStrict)
353
1
2
                delete $CORE::GLOBAL::{$bname};
354        }
355
356        # Pop exactly one meta entry to mirror the mock stack.
357        # Earlier code deleted the entire key; that wiped meta for all layers
358        # still on the stack after a partial unmock.
359
95
95
74
79
        pop @{ $mock_meta{$full_method} };
360
361        # Clean up empty tracking structures
362
95
95
109
107
        unless (@{ $mocked{$full_method} }) {
363
75
55
                delete $mocked{$full_method};
364
75
64
                delete $mock_meta{$full_method};
365        }
366
367
95
108
        return;
368}
369
370 - 401
=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
402
403sub before {
404
34
114154
        my ($arg1, $arg2, $arg3) = @_;
405
406
34
26
        my ($package, $method, $hook);
407
34
137
        if (defined $arg1 && !defined $arg3 && $arg1 =~ /^(.*)::([^:]+)$/) {
408
29
43
                ($package, $method, $hook) = ($1, $2, $arg2);
409        } else {
410
5
7
                ($package, $method, $hook) = ($arg1, $arg2, $arg3);
411        }
412
413
34
93
        croak 'Package, method and hook are required for before()'
414                unless $package && $method && ref($hook) eq 'CODE';
415
416
29
26
        my $full_method = "${package}::${method}";
417
29
20
        my $orig;
418        {
419
23
23
23
29
55
26
3057
17
                no strict 'refs';    ## no critic (ProhibitNoStrict)
420
29
29
18
46
                $orig = \&{$full_method};
421        }
422
423
29
26
        local $TYPE = _T_BEFORE;
424        mock($package, $method, sub {
425
28
86
                my @args = @_;
426
28
28
                $hook->(@args);
427
27
47
                if (wantarray) {
428
3
3
                        return $orig->(@args);
429                } elsif (defined wantarray) {
430
14
22
                        return scalar $orig->(@args);
431                } else {
432
10
30
                        $orig->(@args);
433
10
11
                        return;
434                }
435
29
53
        });
436
437
29
50
        return;
438}
439
440 - 475
=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
476
477sub after {
478
33
26199
        my ($arg1, $arg2, $arg3) = @_;
479
480
33
28
        my ($package, $method, $hook);
481
33
123
        if (defined $arg1 && !defined $arg3 && $arg1 =~ /^(.*)::([^:]+)$/) {
482
28
39
                ($package, $method, $hook) = ($1, $2, $arg2);
483        } else {
484
5
6
                ($package, $method, $hook) = ($arg1, $arg2, $arg3);
485        }
486
487
33
112
        croak 'Package, method and hook are required for after()'
488                unless $package && $method && ref($hook) eq 'CODE';
489
490
28
27
        my $full_method = "${package}::${method}";
491
28
17
        my $orig;
492        {
493
23
23
23
28
121
39
3203
20
                no strict 'refs';    ## no critic (ProhibitNoStrict)
494
28
28
13
43
                $orig = \&{$full_method};
495        }
496
497
28
23
        local $TYPE = _T_AFTER;
498        mock($package, $method, sub {
499
29
132
                my @args = @_;
500
29
31
                if (wantarray) {
501
3
3
                        my @ret = $orig->(@args);
502
3
8
                        $hook->(@args);
503
3
6
                        return @ret;
504                } elsif (defined wantarray) {
505
16
48
                        my $ret = $orig->(@args);
506
16
28
                        $hook->(@args);
507
15
23
                        return $ret;
508                } else {
509
10
9
                        $orig->(@args);
510
8
29
                        $hook->(@args);
511
8
12
                        return;
512                }
513
28
53
        });
514
515
28
32
        return;
516}
517
518 - 562
=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
563
564sub around {
565
36
26193
        my ($arg1, $arg2, $arg3) = @_;
566
567
36
22
        my ($package, $method, $hook);
568
36
128
        if (defined $arg1 && !defined $arg3 && $arg1 =~ /^(.*)::([^:]+)$/) {
569
31
44
                ($package, $method, $hook) = ($1, $2, $arg2);
570        } else {
571
5
7
                ($package, $method, $hook) = ($arg1, $arg2, $arg3);
572        }
573
574
36
107
        croak 'Package, method and hook are required for around()'
575                unless $package && $method && ref($hook) eq 'CODE';
576
577
31
24
        my $full_method = "${package}::${method}";
578
31
17
        my $orig;
579        {
580
23
23
23
31
57
15
5487
20
                no strict 'refs';    ## no critic (ProhibitNoStrict)
581
31
31
14
41
                $orig = \&{$full_method};
582        }
583
584
31
29
        local $TYPE = _T_AROUND;
585
31
29
53
358
        mock($package, $method, sub { $hook->($orig, @_) });
586
587
31
32
        return;
588}
589
590 - 628
=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
629
630sub mock_scoped {
631
38
203495
        my @args = @_;
632
633
38
30
        my @pairs;
634
635
38
174
        if (@args == 2 && ref($args[1]) eq 'CODE') {
636
13
18
                my ($pkg, $meth) = _parse_target($args[0]);
637
13
24
                push @pairs, [ $pkg, $meth, $args[1] ];
638
639        } elsif (@args == 3 && !ref($args[1]) && ref($args[2]) eq 'CODE') {
640
4
6
                push @pairs, [ $args[0], $args[1], $args[2] ];
641
642        } elsif (@args >= 4 && (@args % 2) == 0 && ref($args[1]) eq 'CODE') {
643
5
4
                my @a = @args;
644
5
8
                while (@a) {
645
11
29
                        my ($target, $code) = splice @a, 0, 2;
646
11
18
                        croak "mock_scoped: expected coderef for '$target'"
647                                unless ref($code) eq 'CODE';
648
10
15
                        my ($pkg, $meth) = _parse_target($target);
649
10
17
                        push @pairs, [ $pkg, $meth, $code ];
650                }
651
652        } elsif (@args >= 5 && (@args % 2) == 1 && ref($args[2]) eq 'CODE') {
653
7
7
                my @a   = @args;
654
7
8
                my $pkg = shift @a;
655
7
7
                while (@a) {
656
15
18
                        my ($meth, $code) = splice @a, 0, 2;
657
15
22
                        croak "mock_scoped: expected coderef for method '$meth'"
658                                unless ref($code) eq 'CODE';
659
14
16
                        push @pairs, [ $pkg, $meth, $code ];
660                }
661
662        } else {
663
9
39
                croak 'mock_scoped: unrecognised argument form';
664        }
665
666
27
23
        my @full_methods;
667        {
668
27
27
12
27
                local $TYPE = _T_MOCK_SCOPED;
669
27
25
                for my $pair (@pairs) {
670
39
39
23
41
                        my ($pkg, $meth, $code) = @{$pair};
671
39
43
                        mock($pkg, $meth, $code);
672
39
67
                        push @full_methods, "${pkg}::${meth}";
673                }
674        }
675
676
27
61
        return Test::Mockingbird::Guard->new(@full_methods);
677}
678
679 - 706
=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
707
708sub spy {
709
120
211521
        my ($package, $method) = _parse_target(@_);
710
711
120
218
        croak 'Package and method are required for spying'
712                unless $package && $method;
713
714
116
95
        my $full_method = "${package}::${method}";
715
716        # Capture current implementation (or undef stub if none exists).
717        # We never delete the GV; we always restore by assigning back to *{},
718        # preserving the GV so compiled direct calls remain valid.
719
116
75
        my ($orig, $orig_existed);
720        {
721
23
23
23
116
60
18
1376
82
                no strict 'refs';    ## no critic (ProhibitNoStrict)
722
116
116
72
187
                $orig_existed = defined(&{$full_method}) ? 1 : 0;
723
116
116
73
126
                $orig         = \&{$full_method};
724        }
725
116
116
77
109
        push @{ $mocked{$full_method} }, $orig;
726
727
116
77
        my @calls;
728
729        my $wrapper = sub {
730
131
1712
                push @calls,    [ $full_method, @_ ];
731
131
86
                push @call_log, $full_method;
732                # FIXME: check for recursive calls
733
131
113
                return $orig->(@_);
734
116
174
        };
735
736        # Preserve the original's prototype to suppress "Prototype mismatch"
737        # warnings and ensure '_'-prototype functions (stat, lstat, etc.) bind
738        # $_ correctly at the call site when wrapped.
739
116
83
        my $orig_proto = prototype($orig);
740
116
100
        if (defined $orig_proto) {
741
1
2
                &Scalar::Util::set_prototype($wrapper, $orig_proto);
742        }
743
744        {
745
23
23
23
116
45
13
391
64
                no warnings 'redefine', 'prototype';
746
23
23
23
35
16
2401
                no strict 'refs';    ## no critic (ProhibitNoStrict)
747
116
116
64
155
                *{$full_method} = $wrapper;
748        }
749
750
116
116
68
123
        push @{ $mock_meta{$full_method} }, {
751                type             => _T_SPY,
752                installed_at     => _caller_info(),
753                original_existed => $orig_existed,
754        };
755
756
116
48
177
1765
        return sub { @calls };
757}
758
759 - 785
=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

=cut
786
787sub inject {
788
55
103537
        my ($package, $dependency, $mock_object);
789
790        # Discriminate shorthand (2 args) from longhand (3 args) by argument
791        # count rather than definedness of the third arg so that inject(Pkg,
792        # Dep, undef) -- injecting undef -- is correctly handled.
793
55
136
        if (@_ == 2 && defined $_[0] && $_[0] =~ /^(.*)::([^:]+)$/) {
794
12
22
                ($package, $dependency, $mock_object) = ($1, $2, $_[1]);
795        } else {
796
43
56
                ($package, $dependency, $mock_object) = @_;
797        }
798
799
55
116
        croak 'Package and dependency are required for injection'
800                unless $package && $dependency;
801
802
46
42
        my $full = "${package}::${dependency}";
803
804
46
30
        my ($orig, $orig_existed);
805        {
806
23
23
23
46
54
19
823
37
                no strict 'refs';    ## no critic (ProhibitNoStrict)
807
46
46
23
99
                $orig_existed = defined(&{$full}) ? 1 : 0;
808
46
46
48
47
                $orig         = \&{$full};
809        }
810
46
46
35
48
        push @{ $mocked{$full} }, $orig;
811
812
46
35
64
693
        my $wrapper = sub { $mock_object };
813
814        {
815
23
23
23
46
37
39
416
28
                no warnings 'redefine';
816
23
23
23
37
16
6074
                no strict 'refs';    ## no critic (ProhibitNoStrict)
817
46
46
26
61
                *{$full} = $wrapper;
818        }
819
820        # inject() respects $TYPE so that inject_all() or any future wrapper
821        # can label the layer differently (though 'inject' is the sensible default).
822
46
46
33
91
        push @{ $mock_meta{$full} }, {
823                type             => $TYPE // _T_INJECT,
824                installed_at     => _caller_info(),
825                original_existed => $orig_existed,
826        };
827
828
46
56
        return;
829}
830
831 - 859
=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
860
861sub inject_all {
862
27
95704
        my ($package, $deps) = @_;
863
864
27
85
        croak 'inject_all requires a package name'
865                unless defined $package && length $package;
866
867
19
48
        croak 'inject_all requires a hashref of dependencies'
868                unless ref $deps eq 'HASH';
869
870
12
27
        inject($package, $_, $deps->{$_}) for keys %$deps;
871
872
12
16
        return;
873}
874
875 - 906
=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
907
908sub intercept_new {
909
37
114826
        my ($class, $factory) = @_;
910
911
37
109
        croak 'intercept_new requires a class name'
912                unless defined $class && length $class;
913
29
60
        croak 'intercept_new requires a replacement object or coderef'
914                if @_ < 2;
915
916        my $replacement = ref($factory) eq 'CODE'
917                ? $factory
918
25
20
36
49
                : sub { $factory };
919
920
25
25
        local $TYPE = _T_INTERCEPT_NEW;
921
25
33
        mock("${class}::new", $replacement);
922
923
25
26
        return;
924}
925
926 - 989
=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.

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 build CORE::$name delegator: ..."         -- eval failed

=cut
990
991sub mock_core {
992
14
89378
        my ($name, $replacement) = @_;
993
994
14
44
        croak 'mock_core requires a builtin name and a replacement coderef'
995                unless defined $name && ref($replacement) eq 'CODE';
996
997
11
11
        $name =~ s/^CORE:://;    # tolerate an optional 'CORE::' prefix
998
999
11
30
        croak "mock_core: '$name' is not a valid identifier"
1000                unless $name =~ /^\w+$/;
1001
10
12
        croak "mock_core: '$name' is not an overridable Perl builtin"
1002                unless _is_core_overridable($name);
1003
1004
9
9
9
7
19
5
        my $core_proto = eval { my $p = prototype("CORE::$name"); $p };
1005
1006        # Build a delegator that calls CORE::$name directly.  Must be eval'd
1007        # because CORE:: names are compile-time constructs -- no runtime coderef
1008        # exists.  Calling CORE:: directly bypasses any other CORE::GLOBAL override.
1009        # For '_' prototype (e.g. stat), pass $_[0] explicitly to avoid the
1010        # "Array passed to stat will be coerced to a scalar" compiler warning that
1011        # fires when @_ is passed to a single-arg builtin.
1012
9
20
        my $args      = (defined $core_proto && $core_proto eq '_') ? '$_[0]' : '@_';
1013
9
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
1
366
3
1
28
3
1
24
4
1
22
3
1
22
3
1
20
2
1
21
3
0
26
2
1
24
2
1
20
        my $call_core = eval "sub { no warnings 'syntax'; CORE::$name($args) }";  ## no critic (ProhibitStringyEval)
1014
9
14
        croak "mock_core: cannot build CORE::$name delegator: $@" if $@;
1015
1016        # Wrap in an around-style closure and stamp the CORE prototype onto it
1017        # so call-site argument binding (e.g. '_' reads $_ when arg omitted) works.
1018        # List-op builtins (warn, die, print ...) have no traditional prototype
1019        # string (prototype("CORE::warn") returns undef), but Perl's CORE::GLOBAL
1020        # mechanism expects the override to carry '@'.  Use '@' as the fallback so
1021        # the wrapper's prototype matches and suppresses "Prototype mismatch" at
1022        # eval-compile time.
1023
9
8
14
235
        my $wrapper        = sub { $replacement->($call_core, @_) };
1024
9
29
        my $effective_proto = defined($core_proto) ? $core_proto : '@';
1025
9
23
        &Scalar::Util::set_prototype($wrapper, $effective_proto);
1026
1027        # Install in CORE::GLOBAL -- Perl's documented mechanism for global
1028        # builtin overrides.  Track under the full name so unmock/restore_all work.
1029
9
7
        my $full = "CORE::GLOBAL::$name";
1030
1031
9
6
        my ($orig, $orig_existed);
1032        {
1033
23
23
23
9
62
14
759
7
                no strict 'refs';    ## no critic (ProhibitNoStrict)
1034
9
9
4
29
                $orig_existed = defined(&{$full}) ? 1 : 0;
1035                # When no prior override existed, push undef rather than \&{$full}.
1036                # \&{$full} auto-vivifies the stash entry and returns an undef-stub CV.
1037                # Reinstating that stub on restore would leave a CORE::GLOBAL entry
1038                # whose non-NULL CV pointer makes Perl treat it as an active override,
1039                # preventing fallback to the real builtin.  _drain_and_restore detects
1040                # undef and deletes the stash entry instead.
1041
9
1
9
2
                $orig = $orig_existed ? \&{$full} : undef;
1042        }
1043
9
9
6
11
        push @{ $mocked{$full} }, $orig;
1044
1045        {
1046
23
23
23
9
45
11
391
7
                no warnings 'redefine', 'prototype';
1047
23
23
23
38
21
12265
                no strict 'refs';    ## no critic (ProhibitNoStrict)
1048
9
9
4
16
                *{$full} = $wrapper;
1049        }
1050
1051
9
9
10
22
        push @{ $mock_meta{$full} }, {
1052                type             => $TYPE // _T_MOCK_CORE,
1053                installed_at     => _caller_info(),
1054                original_existed => $orig_existed,
1055        };
1056
1057
9
11
        return;
1058}
1059
1060 - 1081
=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, only mocks whose fully-qualified names
begin with that package are restored. The call-order log is pruned to
remove entries for the restored package.

=head3 API SPECIFICATION

=head4 Input

    package -- Str, optional

=head4 Output

    returns: undef

=cut
1082
1083sub restore_all {
1084
360
107185
        my $arg = $_[0];
1085
1086
360
409
        if (defined $arg) {
1087
11
10
                my $package = $arg;
1088
1089
11
11
                for my $full_method (keys %mocked) {
1090
18
102
                        next unless $full_method =~ /^\Q$package\E::/;
1091
10
14
                        _drain_and_restore($full_method);
1092                        # _drain_and_restore explicitly skips hash cleanup; do it here
1093                        # to match the behaviour of the global form (%mocked = (); etc.).
1094
10
9
                        delete $mocked{$full_method};
1095
10
14
                        delete $mock_meta{$full_method};
1096                }
1097
1098                # Remove call_log entries for the restored package
1099
11
7
12
24
                @call_log = grep { $_ !~ /^\Q$package\E::/ } @call_log;
1100
1101
11
14
                return;
1102        }
1103
1104        # Global restore: revert every tracked method to its saved state
1105
349
516
        _drain_and_restore($_) for keys %mocked;
1106
1107
349
389
        %mocked    = ();
1108
349
471
        %mock_meta = ();
1109
349
260
        @call_log  = ();
1110
1111
349
409
        return;
1112}
1113
1114 - 1136
=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
1137
1138sub restore {
1139
13
9846
        my $target = $_[0];
1140
1141
13
29
        croak 'restore requires a target' unless defined $target;
1142
1143
10
14
        my ($package, $method) = _parse_target($target);
1144
10
14
        my $full_method = "${package}::${method}";
1145
1146
10
28
        return unless exists $mocked{$full_method};
1147
1148
6
6
        _drain_and_restore($full_method);
1149
6
6
        delete $mocked{$full_method};
1150
6
8
        delete $mock_meta{$full_method};
1151
1152
6
6
        return;
1153}
1154
1155 - 1176
=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
1177
1178sub mock_return {
1179
38
116217
        my ($target, $value) = @_;
1180
1181
38
54
        croak 'mock_return requires a target and a value' unless defined $target;
1182
1183
35
28
        local $TYPE = _T_MOCK_RETURN;
1184
35
21
58
394
        mock $target => sub { $value };
1185
1186
35
32
        return;
1187}
1188
1189 - 1210
=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
1211
1212sub mock_exception {
1213
18
12672
        my ($target, $message) = @_;
1214
1215
18
73
        croak 'mock_exception requires a target and an exception message'
1216                unless defined $target && defined $message;
1217
1218
11
11
        local $TYPE = _T_MOCK_EXCEPT;
1219
11
8
22
1043
        mock $target => sub { croak $message };
1220
1221
11
11
        return;
1222}
1223
1224 - 1246
=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
1247
1248sub mock_sequence {
1249
20
17687
        my ($target, @values) = @_;
1250
1251
20
58
        croak 'mock_sequence requires a target and at least one value'
1252                unless defined $target && @values;
1253
1254
16
18
        my @queue = @values;
1255
1256
16
15
        local $TYPE = _T_MOCK_SEQ;
1257        mock $target => sub {
1258
30
66
                return $queue[0] if @queue == 1;
1259
8
16
                return shift @queue;
1260
16
28
        };
1261
1262
16
35
        return;
1263}
1264
1265 - 1297
=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
1298
1299sub mock_once {
1300
24
21196
        my ($target, $code) = @_;
1301
1302
24
67
        croak 'mock_once requires a target and a coderef'
1303                unless defined $target && ref($code) eq 'CODE';
1304
1305
19
24
        my ($package, $method) = _parse_target($target);
1306
1307        my $wrapper = sub {
1308
16
36
                my @result = $code->(@_);
1309
16
31
                Test::Mockingbird::unmock($package, $method);
1310
16
50
                return wantarray ? @result : $result[0];
1311
19
31
        };
1312
1313
19
17
        local $TYPE = _T_MOCK_ONCE;
1314
19
22
        mock $target => $wrapper;
1315
1316
19
19
        return;
1317}
1318
1319 - 1342
=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
1343
1344sub assert_call_order {
1345
33
5046
        my @expected = @_;
1346
1347
33
55
        croak 'assert_call_order requires at least two method names'
1348                unless @expected >= 2;
1349
1350
29
29
        my $pos = 0;
1351
29
25
        for my $logged (@call_log) {
1352
59
61
                if ($logged eq $expected[$pos]) {
1353
47
26
                        $pos++;
1354
47
46
                        last if $pos == @expected;
1355                }
1356        }
1357
1358
29
25
        my $ok = ($pos == @expected);
1359
1360
29
70
        require Test::More;
1361
29
26
        if ($ok) {
1362
19
37
                Test::More::pass("call order: " . join(' -> ', @expected));
1363        } else {
1364
10
25
                Test::More::fail("call order: " . join(' -> ', @expected));
1365
10
6231
                Test::More::diag(
1366                        "Expected '$expected[$pos]' next but it was not in the call log"
1367                );
1368        }
1369
1370
29
6433
        return $ok;
1371}
1372
1373 - 1391
=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
1392
1393sub clear_call_log {
1394
7
7013
        @call_log = ();
1395
7
13
        return;
1396}
1397
1398# _record_call -- Private helper
1399#
1400# Purpose:      Append a fully-qualified method name to the call-order log.
1401#               Used by Test::Mockingbird::Async to participate in
1402#               assert_call_order() without crossing the lexical boundary of
1403#               @call_log.
1404# Entry:        $_[0] -- Str, fully-qualified method name
1405# Exit:         undef
1406# Side effects: Appends to @call_log
1407sub _record_call {
1408
1
3
        push @call_log, $_[0];
1409
1
1
        return;
1410}
1411
1412 - 1432
=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
1433
1434sub diagnose_mocks {
1435
85
6661
        my %report;
1436
1437
85
126
        for my $full_method (sort keys %mocked) {
1438
81
106
                my $layers = $mock_meta{$full_method} // [];
1439                $report{$full_method} = {
1440
81
248
                        depth            => scalar @{ $mocked{$full_method} },
1441                        layers           => [ @$layers ],
1442                        # original_existed reflects whether the method existed before the
1443                        # FIRST mock layer was installed (stored in the bottom-most meta entry)
1444
81
52
                        original_existed => (@$layers && $layers->[0]{original_existed}) ? 1 : 0,
1445                };
1446        }
1447
1448
85
98
        return \%report;
1449}
1450
1451 - 1465
=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
1466
1467sub diagnose_mocks_pretty {
1468
8
76548
        my $diag = diagnose_mocks();
1469
8
10
        my @out;
1470
1471
8
10
        for my $full_method (sort keys %$diag) {
1472
10
6
                my $entry = $diag->{$full_method};
1473
10
9
                push @out, "$full_method:";
1474
10
12
                push @out, "  depth: $entry->{depth}";
1475
10
9
                push @out, "  original_existed: $entry->{original_existed}";
1476
10
10
6
10
                for my $layer (@{ $entry->{layers} }) {
1477                        push @out, sprintf "  - type: %-14s installed_at: %s",
1478
10
21
                                $layer->{type}, $layer->{installed_at};
1479                }
1480
10
9
                push @out, '';
1481        }
1482
1483
8
24
        return join "\n", @out;
1484}
1485
1486# _drain_and_restore -- Private helper
1487#
1488# Purpose:      Pop all layers from the mock stack for a single target and
1489#               restore the bottom-most saved coderef to the symbol table.
1490#               Does NOT clean up %mocked or %mock_meta -- callers must do
1491#               that themselves.
1492# Entry:        $_[0] -- Str, fully-qualified method name
1493# Exit:         undef
1494# Side effects: Modifies the symbol table for the target.
1495sub _drain_and_restore {
1496
381
235
        my $full_method = $_[0];
1497
1498
381
248
        my $final_prev;
1499
381
850
207
761
        while (@{ $mocked{$full_method} }) {
1500
469
469
257
460
                $final_prev = pop @{ $mocked{$full_method} };
1501        }
1502
1503        # Restore the original (bottom of stack) to the SAME GV that compiled
1504        # calls hold.  We never delete the GV because compiled direct-call ops
1505        # cache the GV at compile time; a new GV would be invisible to them.
1506
381
322
        if (defined $final_prev) {
1507
23
23
23
63
12
411
                no warnings 'redefine';
1508
23
23
23
33
18
1026
                no strict 'refs';    ## no critic (ProhibitNoStrict)
1509
374
374
231
1084
                *{$full_method} = $final_prev;
1510        } elsif ($full_method =~ /^CORE::GLOBAL::/) {
1511                # No prior CORE::GLOBAL override existed (mock_core pushed undef).
1512                # Delete the stash entry entirely so Perl's builtin lookup falls back
1513                # to the real CORE function.  Reinstating the undef-stub CV is not
1514                # sufficient: Perl treats any non-NULL CV in CORE::GLOBAL as an active
1515                # user override, so the real builtin would never be reached.
1516
7
27
                my ($bname) = ($full_method =~ /::([^:]+)$/);
1517
23
23
23
54
16
6323
                no strict 'refs';    ## no critic (ProhibitNoStrict)
1518
7
12
                delete $CORE::GLOBAL::{$bname};
1519        }
1520
1521
381
455
        return;
1522}
1523
1524# _parse_target -- Private helper
1525#
1526# Purpose:      Normalise both shorthand ('Pkg::method') and longhand
1527#               ('Pkg', 'method') call forms into a ($package, $method) pair.
1528# Entry:        @_ -- one arg for shorthand, two args for longhand
1529# Exit:         ($package, $method) -- list of two strings
1530sub _parse_target {
1531
229
86150
        my ($arg1, $arg2) = @_;
1532
1533        # Shorthand: single 'Pkg::method' string -- arg2 is absent (undef)
1534
229
754
        if (defined $arg1 && !defined $arg2 && $arg1 =~ /^(.*)::([^:]+)$/) {
1535
184
323
                return ($1, $2);
1536        }
1537
1538
45
52
        return ($arg1, $arg2);
1539}
1540
1541# _caller_info -- Private helper
1542#
1543# Purpose:      Walk up the call stack to find the first frame outside any
1544#               Test::Mockingbird namespace.  Returns a human-readable
1545#               "file line N" string for use in installed_at diagnostics.
1546#               This ensures that sugar functions (mock_return, mock_once,
1547#               etc.) report the user's call site, not their own location.
1548# Entry:        none
1549# Exit:         Str, e.g. "t/my_test.t line 42"
1550sub _caller_info {
1551
567
1764
        my $level = 1;
1552
567
602
        while (my @info = caller($level)) {
1553
897
6151
                last unless $info[0] =~ /^Test::Mockingbird/;
1554
330
431
                $level++;
1555        }
1556
567
635
        my @info = caller($level);
1557
567
3889
        return defined $info[1] ? "$info[1] line $info[2]" : '(unknown)';
1558}
1559
1560# _get_prototype -- Private helper
1561#
1562# Return the prototype string of a named sub, if any.
1563#
1564# Entry:        $_[0] -- Str, fully-qualified sub name
1565# Exit:         Str or undef
1566sub _get_prototype {
1567
11
9347
        my $full = $_[0];
1568
1569        # All components (package segments and the sub name itself) must start
1570        # with a letter or underscore -- Perl identifiers cannot begin with a digit.
1571
11
53
        croak "Invalid fully-qualified name '$full'"
1572                unless $full =~ /^[A-Za-z_]\w*(?:::[A-Za-z_]\w*)+$/;
1573
1574
6
13
        my ($pkg, $sub) = $full =~ /^(.*)::([^:]+)$/;
1575
6
28
        my $code = $pkg->can($sub) or return;
1576
4
9
        return prototype($code);
1577}
1578
1579# _is_core_overridable -- Private helper
1580#
1581# Determine whether a bare name refers to a CORE builtin that
1582#       Perl allows packages to shadow with a user sub.
1583# Entry:        $_[0] -- Str, simple identifier (no 'CORE::' prefix)
1584# Exit:         Bool -- true if prototype("CORE::$name") succeeds (even if
1585#               the returned prototype is undef, meaning "no prototype")
1586sub _is_core_overridable {
1587
10
7
        my $name = $_[0];
1588
10
6
        local $@;
1589
10
10
9
9
33
9
        eval { my $p = prototype("CORE::$name"); 1 };
1590
10
21
        return !$@;
1591}
1592
1593 - 1828
=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

=head2 mock

    mock ≙
      âˆ€ target : Str; replacement : CodeRef •
        pre  target ≠ '' ∧ defined(replacement)
        post mocked'[target] = ⟨saved(target)⟩ ⌢ mocked[target]
             âˆ§ sym_table'[target].CODE = replacement
             âˆ§ prototype(replacement) = prototype(saved(target))

=head2 unmock

    unmock ≙
      âˆ€ target : Str •
        let prev = head(mocked[target]) •
          post mocked'[target] = tail(mocked[target])
               âˆ§ sym_table'[target].CODE = prev
               âˆ§ mock_meta'[target] = tail(mock_meta[target])

=head2 before

    before ≙
      âˆ€ target : Str; hook : CodeRef •
        pre  target ≠ '' ∧ ref(hook) = 'CODE'
        let orig = sym_table[target].CODE •
          post sym_table'[target].CODE = wrapper
               âˆ§ wrapper(@args) ≙ hook(@args); orig(@args)

=head2 after

    after ≙
      âˆ€ target : Str; hook : CodeRef •
        pre  target ≠ '' ∧ ref(hook) = 'CODE'
        let orig = sym_table[target].CODE •
          post sym_table'[target].CODE = wrapper
               âˆ§ wrapper(@args) ≙ let ret = orig(@args) • hook(@args); ret

=head2 around

    around ≙
      âˆ€ target : Str; hook : CodeRef •
        pre  target ≠ '' ∧ ref(hook) = 'CODE'
        let orig = sym_table[target].CODE •
          post sym_table'[target].CODE = wrapper
               âˆ§ wrapper(@args) ≙ hook(orig, @args)

=head2 mock_core

    mock_core ≙
      âˆ€ name : Str; replacement : CodeRef •
        pre  _is_core_overridable(name) ∧ ref(replacement) = 'CODE'
        let  call_core = eval("sub { CORE::name(@_) }") •
        let  wrapper   = sub { replacement(call_core, @_) } •
          post CORE::GLOBAL::name.CODE = wrapper
               âˆ§ prototype(wrapper) = prototype(CORE::name)
               âˆ§ mocked'["CORE::GLOBAL::name"] = ⟨wrapper⟩ ⌢ mocked["CORE::GLOBAL::name"]

=head2 mock_scoped

    mock_scoped ≙
      install all mocks via mock()
      âˆ§ return Guard(full_methods)
      âˆ§ Guard.DESTROY ⇒ ∀ m ∈ full_methods • unmock(m)

=head2 spy

    spy ≙
      âˆ€ target : Str •
        pre  defined(target)
        post sym_table'[target].CODE = wrapper(orig)
             âˆ§ wrapper: @args → (calls' = calls ⌢ ⟨[target, @args]⟩ ∧ orig(@args))

=head2 inject

    inject ≙
      âˆ€ pkg : Str; dep : Str; val : Any •
        pre  pkg ≠ '' ∧ dep ≠ ''
        post sym_table'["${pkg}::${dep}"].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 restore_all

    restore_all ≙
      global: mocked' = {} ∧ mock_meta' = {} ∧ call_log' = []
      scoped: ∀ target ∈ dom(mocked) • target =~ /^pkg::/ ⇒ unmock_all(target)
              âˆ§ call_log' = [ e ∈ call_log | e !~ /^pkg::/ ]

=head2 restore

    restore ≙
      âˆ€ target : Str •
        pre  defined(target)
        post mocked[target] = []

=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, layers } | target ∈ dom(mocked) }

=head2 diagnose_mocks_pretty

    diagnose_mocks_pretty ≙ stringify(diagnose_mocks())

=head2 before

    before ≙
      âˆ€ target : Str; hook : CodeRef •
        pre  target ≠ '' ∧ ref(hook) = 'CODE'
        let orig = sym_table[target].CODE •
          post sym_table'[target].CODE = wrapper
               âˆ§ wrapper(@args) ≙ hook(@args); orig(@args)

=head2 after

    after ≙
      âˆ€ target : Str; hook : CodeRef •
        pre  target ≠ '' ∧ ref(hook) = 'CODE'
        let orig = sym_table[target].CODE •
          post sym_table'[target].CODE = wrapper
               âˆ§ wrapper(@args) ≙
                   let ret = orig(@args) •
                   hook(@args);
                   ret

=head2 around

    around ≙
      âˆ€ target : Str; hook : CodeRef •
        pre  target ≠ '' ∧ ref(hook) = 'CODE'
        let orig = sym_table[target].CODE •
          post sym_table'[target].CODE = wrapper
               âˆ§ wrapper(@args) ≙ hook(orig, @args)

=head2 mock_core

    mock_core ≙
      âˆ€ name : Str; replacement : CodeRef •
        pre  _is_core_overridable(name) ∧ ref(replacement) = 'CODE'
        let  call_core = eval("sub { CORE::name(@_) }") •
        let  wrapper   = sub { replacement(call_core, @_) } •
          post CORE::GLOBAL::name.CODE = wrapper
               âˆ§ prototype(wrapper) = prototype(CORE::name)
               âˆ§ mocked'["CORE::GLOBAL::name"] = ⟨wrapper⟩ ⌢ mocked["CORE::GLOBAL::name"]

=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
1829
18301;
1831
1832package Test::Mockingbird::Guard;
1833
1834# Guard object returned by mock_scoped.  Stores a list of fully-qualified
1835# method names and calls unmock() on each when destroyed.
1836
1837sub new {
1838
27
29
        my ($class, @full_methods) = @_;
1839
27
78
        return bless { full_methods => \@full_methods }, $class;
1840}
1841
1842sub DESTROY {
1843
27
12942
        my $self = $_[0];
1844
27
27
21
48
        Test::Mockingbird::unmock($_) for @{ $self->{full_methods} };
1845
27
34
        return;
1846}
1847
18481;