File Coverage

blib/lib/Test/Mockingbird.pm
Criterion Covered Total %
statement 240 255 94.1
branch 37 44 84.0
condition 34 55 61.8
subroutine 44 45 97.7
pod 13 13 100.0
total 368 412 89.3


line stmt bran cond sub pod time code
1             package Test::Mockingbird;
2              
3 13     13   2516714 use strict;
  13         24  
  13         487  
4 13     13   61 use warnings;
  13         21  
  13         694  
5              
6             # TODO: Look into Sub::Install
7              
8 13     13   64 use Carp qw(croak);
  13         23  
  13         685  
9 13     13   62 use Exporter 'import';
  13         34  
  13         3857  
10              
11             our @EXPORT = qw(
12             mock
13             unmock
14             mock_scoped
15             spy
16             inject
17             restore
18             restore_all
19             mock_return
20             mock_exception
21             mock_sequence
22             mock_once
23             diagnose_mocks
24             diagnose_mocks_pretty
25             );
26              
27             # Store mocked data
28             my %mocked; # becomes: method => [ stack of backups ]
29             my %mock_meta; # full_method => [ { type => ..., installed_at => ... }, ... ]
30              
31             =head1 NAME
32              
33             Test::Mockingbird - Advanced mocking library for Perl with support for dependency injection and spies
34              
35             =head1 VERSION
36              
37             Version 0.06
38              
39             =cut
40              
41             our $VERSION = '0.06';
42              
43             =head1 SYNOPSIS
44              
45             use Test::Mockingbird;
46              
47             # Mocking
48             Test::Mockingbird::mock('My::Module', 'method', sub { return 'mocked!' });
49              
50             # Spying
51             my $spy = Test::Mockingbird::spy('My::Module', 'method');
52             My::Module::method('arg1', 'arg2');
53             my @calls = $spy->(); # Get captured calls
54              
55             # Dependency Injection
56             Test::Mockingbird::inject('My::Module', 'Dependency', $mock_object);
57              
58             # Unmocking
59             Test::Mockingbird::unmock('My::Module', 'method');
60              
61             # Restore everything
62             Test::Mockingbird::restore_all();
63              
64             =head1 DESCRIPTION
65              
66             Test::Mockingbird provides powerful mocking, spying, and dependency injection capabilities to streamline testing in Perl.
67              
68             =head1 DIAGNOSTICS
69              
70             Test::Mockingbird provides optional, non-intrusive diagnostic routines
71             that allow inspection of the current mocking state during test execution.
72             These routines are purely observational. They do not modify any mocking
73             behaviour, symbol table entries, or internal state.
74              
75             Diagnostics are useful when debugging complex test suites, verifying
76             mock layering behaviour, or understanding interactions between multiple
77             mocking primitives such as mock, spy, inject, and the sugar functions.
78              
79             =head2 diagnose_mocks
80              
81             Return a structured hashref describing all currently active mock layers.
82             Each entry includes the fully qualified method name, the number of active
83             layers, whether the original method existed, and metadata for each layer
84             (type and installation location). See the diagnose_mocks method for full
85             API details.
86              
87             =head2 diagnose_mocks_pretty
88              
89             Return a human-readable, multi-line string describing all active mock
90             layers. This routine is intended for debugging and inspection during test
91             development. The output format is stable for human consumption but is not
92             guaranteed for machine parsing. See the diagnose_mocks_pretty method for
93             full API details.
94              
95             =head2 Diagnostic Metadata
96              
97             Diagnostic information is recorded automatically whenever a mock layer is
98             successfully installed. Each layer records:
99              
100             * type The category of mock layer (for example: mock, spy,
101             inject, mock_return, mock_exception, mock_sequence,
102             mock_once, mock_scoped)
103              
104             * installed_at The file and line number where the layer was created
105              
106             This metadata is maintained in parallel with the internal mock stack and
107             is automatically cleared when a method is fully restored via unmock or
108             restore_all.
109              
110             Diagnostics never alter the behaviour of the mocking engine and may be
111             safely invoked at any point during a test run.
112              
113             =head1 DEBUGGING EXAMPLES
114              
115             This section provides practical examples of using the diagnostic routines
116             to understand and debug complex mocking behaviour.
117             All examples are safe to run inside test files and do not modify mocking semantics.
118              
119             =head2 Example 1: Inspecting a simple mock
120              
121             {
122             package Demo::One;
123             sub value { 1 }
124             }
125              
126             mock_return 'Demo::One::value' => 42;
127              
128             my $diag = diagnose_mocks();
129             print diagnose_mocks_pretty();
130              
131             The output will resemble:
132              
133             Demo::One::value:
134             depth: 1
135             original_existed: 1
136             - type: mock_return installed_at: t/example.t line 12
137              
138             This confirms that the method has exactly one active mock layer and shows
139             where it was installed.
140              
141             =head2 Example 2: Stacked mocks
142              
143             {
144             package Demo::Two;
145             sub compute { 10 }
146             }
147              
148             mock_return 'Demo::Two::compute' => 20;
149             mock_exception 'Demo::Two::compute' => 'fail';
150              
151             print diagnose_mocks_pretty();
152              
153             Possible output:
154              
155             Demo::Two::compute:
156             depth: 2
157             original_existed: 1
158             - type: mock_return installed_at: t/example.t line 8
159             - type: mock_exception installed_at: t/example.t line 9
160              
161             This shows the order in which layers were applied. The most recent layer
162             appears last.
163              
164             =head2 Example 3: Spies and injected dependencies
165              
166             {
167             package Demo::Three;
168             sub action { 1 }
169             sub dep { 2 }
170             }
171              
172             spy 'Demo::Three::action';
173             inject 'Demo::Three::dep' => sub { 99 };
174              
175             print diagnose_mocks_pretty();
176              
177             Example output:
178              
179             Demo::Three::action:
180             depth: 1
181             original_existed: 1
182             - type: spy installed_at: t/example.t line 7
183              
184             Demo::Three::dep:
185             depth: 1
186             original_existed: 1
187             - type: inject installed_at: t/example.t line 8
188              
189             This confirms that both the spy and the injected dependency are active.
190              
191             =head2 Example 4: After restore_all
192              
193             mock_return 'Demo::Four::x' => 5;
194             restore_all();
195              
196             print diagnose_mocks_pretty();
197              
198             Output:
199              
200             (no output)
201              
202             After restore_all, all diagnostic metadata is cleared along with the
203             mock layers.
204              
205             =head2 Example 5: Using diagnostics inside a failing test
206              
207             When a test fails unexpectedly, adding the following line can help
208             identify the active mocks:
209              
210             diag diagnose_mocks_pretty();
211              
212             This prints the current mocking state into the test output without
213             affecting the test run.
214              
215             =cut
216              
217             =head1 METHODS
218              
219             =head2 mock($package, $method, $replacement)
220              
221             Mocks a method in the specified package.
222             Supports two forms:
223              
224             mock('My::Module', 'method', sub { ... });
225              
226             or the shorthand:
227              
228             mock 'My::Module::method' => sub { ... };
229              
230             =cut
231              
232             sub mock {
233 52     52 1 1130948 my ($arg1, $arg2, $arg3) = @_;
234              
235 52         104 my ($package, $method, $replacement);
236              
237             # ------------------------------------------------------------
238             # New syntax:
239             # mock 'My::Module::method' => sub { ... }
240             # ------------------------------------------------------------
241 52 100 66     602 if (defined $arg1 && !defined $arg3 && $arg1 =~ /^(.*)::([^:]+)$/) {
      66        
242 42         110 $package = $1;
243 42         85 $method = $2;
244 42         87 $replacement = $arg2;
245             } else {
246             # ------------------------------------------------------------
247             # Original syntax:
248             # mock('My::Module', 'method', sub { ... })
249             # ------------------------------------------------------------
250 10         33 ($package, $method, $replacement) = ($arg1, $arg2, $arg3);
251             }
252              
253 52 100 33     350 croak 'Package, method and replacement are required for mocking' unless $package && $method && $replacement;
      66        
254              
255 51         121 my $full_method = "${package}::$method";
256              
257             # Backup original if not already mocked
258 51         73 push @{ $mocked{$full_method} }, \&{$full_method};
  51         143  
  51         242  
259              
260 13     13   101 no warnings 'redefine';
  13         38  
  13         711  
261              
262             {
263             ## no critic (ProhibitNoStrict) # symbolic reference required for mocking
264 13     13   70 no strict 'refs';
  13         18  
  13         4626  
  51         99  
265 51         81 *{$full_method} = $replacement;
  51         232  
266             }
267 51   100     210 my $type = $Test::Mockingbird::TYPE // 'mock';
268              
269 51         120 push @{ $mock_meta{$full_method} }, {
  51         507  
270             type => $type, # 'mock', 'spy', 'inject', etc.
271             installed_at => (caller)[1] . ' line ' . (caller)[2],
272             };
273             }
274              
275             =head2 unmock($package, $method)
276              
277             Restores the original method for a mocked method.
278             Supports two forms:
279              
280             unmock('My::Module', 'method');
281              
282             or the shorthand:
283              
284             unmock 'My::Module::method';
285              
286             =cut
287              
288             sub unmock {
289 15     15 1 9028 my ($arg1, $arg2) = @_;
290              
291 15         29 my ($package, $method);
292              
293 15 100 66     156 if (defined $arg1 && !defined $arg2 && $arg1 =~ /^(.*)::([^:]+)$/) {
      66        
294             # Case 1: unmock 'Pkg::method'
295 9         49 ($package, $method) = ($1, $2);
296             } else {
297             # Case 2: unmock 'Pkg', 'method'
298 6         15 ($package, $method) = ($arg1, $arg2);
299             }
300              
301 15 50 33     72 croak 'Package and method are required for unmocking' unless $package && $method;
302              
303 15         34 my $full_method = "${package}::$method";
304              
305             # Restore previous layer if present
306 15 100 66     81 if (exists $mocked{$full_method} && @{ $mocked{$full_method} }) {
  14         51  
307 14         22 my $prev = pop @{ $mocked{$full_method} };
  14         31  
308              
309 13     13   93 no warnings 'redefine';
  13         19  
  13         724  
310              
311             {
312             ## no critic (ProhibitNoStrict) # symbolic reference required for restore
313 13     13   99 no strict 'refs';
  13         47  
  13         5476  
  14         25  
314 14         21 *{$full_method} = $prev;
  14         79  
315             }
316 14 100       23 delete $mocked{$full_method} unless @{ $mocked{$full_method} };
  14         52  
317 14         70 delete $mock_meta{$full_method};
318             }
319             }
320              
321             =head2 mock_scoped
322              
323             Creates a scoped mock that is automatically restored when it goes out of scope.
324              
325             This behaves like C<mock>, but instead of requiring an explicit call to
326             C<unmock> or C<restore_all>, the mock is reverted automatically when the
327             returned guard object is destroyed.
328              
329             This is useful when you want a mock to apply only within a lexical block:
330              
331             {
332             my $g = mock_scoped 'My::Module::method' => sub { 'mocked' };
333             My::Module::method(); # returns 'mocked'
334             }
335              
336             My::Module::method(); # original behaviour restored
337              
338             Supports both the longhand and shorthand forms:
339              
340             my $g = mock_scoped('My::Module', 'method', sub { ... });
341              
342             my $g = mock_scoped 'My::Module::method' => sub { ... };
343              
344             Returns a guard object whose destruction triggers automatic unmocking.
345              
346             =cut
347              
348             sub mock_scoped {
349 4     4 1 197822 my ($arg1, $arg2, $arg3) = @_;
350              
351             # Reuse mock() to install the mock
352 4         19 mock($arg1, $arg2, $arg3);
353              
354             # Determine full method name using same parsing rules
355              
356 4         132 my ($package, $method) = _parse_target(@_);
357              
358 4         10 my $full_method = "${package}::$method";
359              
360 4         5 push @{ $mock_meta{$full_method} }, {
  4         12  
361             type => 'mock_scoped',
362             installed_at => (caller)[1] . ' line ' . (caller)[2],
363             };
364 4         135 return Test::Mockingbird::Guard->new($full_method);
365             }
366              
367             =head2 spy($package, $method)
368              
369             Wraps a method so that all calls and arguments are recorded.
370             Supports two forms:
371              
372             spy('My::Module', 'method');
373              
374             or the shorthand:
375              
376             spy 'My::Module::method';
377              
378             Returns a coderef which, when invoked, returns the list of captured calls.
379             The original method is preserved and still executed.
380              
381             =cut
382              
383             sub spy {
384 26     26 1 230618 my ($arg1, $arg2) = @_;
385              
386 26         116 my ($package, $method) = _parse_target(@_);
387              
388 26 50 33     127 croak 'Package and method are required for spying' unless $package && $method;
389              
390 26         96 my $full_method = "${package}::$method";
391              
392             # Capture the current implementation BEFORE installing the wrapper
393 26         112 my $orig;
394             {
395             ## no critic (ProhibitNoStrict)
396 13     13   93 no strict 'refs';
  13         32  
  13         1770  
  26         106  
397 26         41 $orig = \&{$full_method};
  26         146  
398             }
399              
400             # Track the original implementation
401 26         60 push @{ $mocked{$full_method} }, $orig;
  26         154  
402              
403 26         62 my @calls;
404              
405             # Wrapper: record call, then delegate to the captured original
406             my $wrapper = sub {
407 34     34   4799 push @calls, [ $full_method, @_ ];
408 34         82 return $orig->(@_);
409 26         137 };
410              
411 13     13   100 no warnings 'redefine';
  13         21  
  13         825  
412             {
413             ## no critic (ProhibitNoStrict)
414 13     13   81 no strict 'refs';
  13         18  
  13         4369  
  26         47  
415 26         78 *{$full_method} = $wrapper;
  26         130  
416             }
417              
418 26         45 push @{ $mock_meta{$full_method} }, {
  26         160  
419             type => 'spy',
420             installed_at => (caller)[1] . ' line ' . (caller)[2],
421             };
422 26     22   1270 return sub { @calls };
  22         814  
423             }
424              
425             =head2 inject($package, $dependency, $mock_object)
426              
427             Injects a mock dependency. Supports two forms:
428              
429             inject('My::Module', 'Dependency', $mock_object);
430              
431             or the shorthand:
432              
433             inject 'My::Module::Dependency' => $mock_object;
434              
435             The injected dependency can be restored with C<restore_all> or C<unmock>.
436              
437             =cut
438              
439             sub inject {
440 7     7 1 264254 my ($arg1, $arg2, $arg3) = @_;
441              
442 7         20 my ($package, $dependency, $mock_object);
443              
444             # ------------------------------------------------------------
445             # New shorthand syntax:
446             # inject 'My::Module::Dependency' => $mock_obj
447             # ------------------------------------------------------------
448 7 100 66     102 if (defined $arg1 && !defined $arg3 && $arg1 =~ /^(.*)::([^:]+)$/) {
      66        
449 5         17 $package = $1;
450 5         31 $dependency = $2;
451 5         12 $mock_object = $arg2;
452             } else {
453             # ------------------------------------------------------------
454             # Original syntax:
455             # inject('My::Module', 'Dependency', $mock_obj)
456             # ------------------------------------------------------------
457 2         8 ($package, $dependency, $mock_object) = ($arg1, $arg2, $arg3);
458             }
459              
460 7 50 33     48 croak 'Package and dependency are required for injection' unless $package && $dependency;
461              
462 7         20 my $full_dependency = "${package}::$dependency";
463              
464 7         12 my $orig;
465              
466             {
467             ## no critic (ProhibitNoStrict) # symbolic reference required
468 13     13   92 no strict 'refs';
  13         22  
  13         1294  
  7         38  
469 7         12 $orig = \&{$full_dependency};
  7         38  
470             }
471              
472 7         18 push @{ $mocked{$full_dependency} }, $orig;
  7         26  
473              
474             # Build the injected dependency wrapper outside the strict-free block
475 7     5   65 my $wrapper = sub { $mock_object };
  5         209  
476              
477 13     13   76 no warnings 'redefine';
  13         20  
  13         950  
478              
479             {
480             ## no critic (ProhibitNoStrict) # symbolic reference required for injection
481 13     13   79 no strict 'refs';
  13         24  
  13         3813  
  7         15  
482 7         15 *{$full_dependency} = $wrapper;
  7         42  
483             }
484 7         17 push @{ $mock_meta{$full_dependency} }, {
  7         41  
485             type => 'inject',
486             installed_at => (caller)[1] . ' line ' . (caller)[2],
487             };
488             }
489              
490             =head2 restore_all()
491              
492             Restores mocked methods and injected dependencies.
493              
494             Called with no arguments, it restores everything:
495              
496             restore_all();
497              
498             You may also restore only a specific package:
499              
500             restore_all 'My::Module';
501              
502             This restores all mocked methods whose fully qualified names begin with
503             C<My::Module::>.
504              
505             =cut
506              
507             sub restore_all {
508 59     59 1 43475 my $arg = $_[0];
509              
510             # ------------------------------------------------------------------
511             # If a package name is provided, restore only methods belonging to
512             # that package. Otherwise, restore everything.
513             # ------------------------------------------------------------------
514              
515 59 100       237 if (defined $arg) {
516 2         8 my $package = $arg;
517              
518 2         9 for my $full_method (keys %mocked) {
519 3 100       58 next unless $full_method =~ /^\Q$package\E::/;
520              
521             # Restore all layers for this method
522 2         7 while (@{ $mocked{$full_method} }) {
  4         16  
523 2         24 my $prev = pop @{ $mocked{$full_method} };
  2         7  
524              
525 13     13   121 no warnings 'redefine';
  13         65  
  13         839  
526              
527             {
528             ## no critic (ProhibitNoStrict) # symbolic reference required for restore
529 13     13   75 no strict 'refs';
  13         29  
  13         1790  
  2         6  
530 2         4 *{$full_method} = $prev;
  2         20  
531             }
532             }
533 2         6 delete $mocked{$full_method};
534 2         9 delete $mock_meta{$full_method};
535             }
536              
537 2         9 return;
538             }
539              
540             # ------------------------------------------------------------------
541             # Global restore: revert every mocked or injected method
542             # ------------------------------------------------------------------
543 57         205 for my $full_method (keys %mocked) {
544 57         90 while (@{ $mocked{$full_method} }) {
  121         341  
545 64         97 my $prev = pop @{ $mocked{$full_method} };
  64         141  
546              
547 13     13   79 no warnings 'redefine';
  13         27  
  13         707  
548              
549             {
550             ## no critic (ProhibitNoStrict) # symbolic reference required for restore
551 13     13   70 no strict 'refs';
  13         29  
  13         6844  
  64         105  
552 64         121 *{$full_method} = $prev;
  64         538  
553             }
554             }
555             }
556              
557             # Clear all tracking
558 57         142 %mocked = ();
559 57         291 %mock_meta = ();
560             }
561              
562             =head2 mock_return
563              
564             Mock a method so that it always returns a fixed value.
565              
566             Takes a single target (either C<'Pkg::method'> or C<('Pkg','method')>) and
567             a value to return. Returns nothing. Side effects: installs a mock layer
568             using L</mock>.
569              
570             =head3 API specification
571              
572             =head4 Input
573              
574             Params::Validate::Strict schema:
575              
576             - C<target>: required, scalar, string; method target in shorthand or longhand form
577             - C<value>: required, any type; value to be returned by the mock
578              
579             =head4 Output
580              
581             Returns::Set schema:
582              
583             - C<return>: undef
584              
585             =cut
586              
587             sub mock_return {
588 11     11 1 36407 my ($target, $value) = @_;
589              
590             # Entry: target must be defined, value may be any defined/undef scalar
591             # Exit: mock layer installed for target, no return value
592             # Side effects: modifies symbol table via mock()
593             # Notes: uses existing mock() parsing and stacking semantics
594 11 100       52 croak 'mock_return requires a target and a value' unless defined $target;
595              
596 10     4   39 my $code = sub { $value };
  4         101  
597              
598 10         22 local $Test::Mockingbird::TYPE = 'mock_return';
599              
600             # MUST use the shorthand form:
601 10         30 return mock $target => $code;
602             }
603              
604             =head2 mock_exception
605              
606             Mock a method so that it always throws an exception.
607              
608             Takes a single target (either C<'Pkg::method'> or C<('Pkg','method')>) and
609             an exception message. Returns nothing. Side effects: installs a mock layer
610             using L</mock>.
611              
612             =head3 API specification
613              
614             =head4 Input
615              
616             Params::Validate::Strict schema:
617              
618             - C<target>: required, scalar, string; method target in shorthand or longhand form
619             - C<message>: required, scalar, string; exception text to C<croak> with
620              
621             =head4 Output
622              
623             Returns::Set schema:
624              
625             - C<return>: undef
626              
627             =cut
628              
629             sub mock_exception {
630 4     4 1 8717 my ($target, $message) = @_;
631              
632             # Entry: target and message must be defined scalars
633             # Exit: mock layer installed for target, no return value
634             # Side effects: modifies symbol table via mock()
635             # Notes: exception is thrown with croak semantics from the mocked method
636              
637 4 100 66     54 croak 'mock_exception requires a target and an exception message' unless defined $target && defined $message;
638              
639 3     2   14 my $code = sub { croak $message }; # Throw on every call
  2         231  
640              
641 3         7 local $Test::Mockingbird::TYPE = 'mock_exception';
642              
643 3         8 return mock($target, $code);
644             }
645              
646             =head2 mock_sequence
647              
648             Mock a method so that it returns a sequence of values over successive calls.
649              
650             Takes a single target (either C<'Pkg::method'> or C<('Pkg','method')>) and
651             one or more values. Returns nothing. Side effects: installs a mock layer
652             using L</mock>. When the sequence is exhausted, the last value is repeated.
653              
654             =head3 API specification
655              
656             =head4 Input
657              
658             Params::Validate::Strict schema:
659              
660             - C<target>: required, scalar, string; method target in shorthand or longhand form
661             - C<values>: required, array; one or more values to be returned in order
662              
663             =head4 Output
664              
665             Returns::Set schema:
666              
667             - C<return>: undef
668              
669             =cut
670              
671             sub mock_sequence {
672 5     5 1 14703 my ($target, @values) = @_;
673              
674             # Entry: target defined, at least one value provided
675             # Exit: mock layer installed for target, no return value
676             # Side effects: modifies symbol table via mock()
677             # Notes: last value is repeated once the sequence is exhausted
678              
679 5 100 66     55 croak 'mock_sequence requires a target and at least one value' unless defined $target && @values;
680              
681 4         13 my @queue = @values; # Local copy of the sequence
682              
683             my $code = sub {
684             # If only one value remains, repeat it
685 6 100   6   170 return $queue[0] if @queue == 1;
686 3         17 return shift @queue;
687 4         19 };
688              
689 4         11 local $Test::Mockingbird::TYPE = 'mock_sequence';
690              
691 4         13 return mock($target, $code);
692             }
693              
694             =head2 mock_once
695              
696             Install a mock that is executed exactly once. After the first call, the
697             previous implementation is automatically restored. This is useful for
698             testing retry logic, fallback behaviour, and state transitions.
699              
700             =head3 API specification
701              
702             =head4 Input (Params::Validate::Strict schema)
703              
704             - C<target>: required, scalar, string; method target in shorthand or longhand form
705             - C<code>: required, coderef; mock implementation to run once
706              
707             =head4 Output (Returns::Set schema)
708              
709             - C<return>: undef
710              
711             =cut
712              
713             sub mock_once {
714 6     6 1 13705 my ($target, $code) = @_;
715              
716             # Entry criteria:
717             # - target must be defined
718             # - code must be a coderef
719 6 100 66     71 croak 'mock_once requires a target and a coderef' unless defined $target && ref($code) eq 'CODE';
720              
721             # Parse target using existing logic
722 5         21 my ($package, $method) = _parse_target($target);
723 5         14 my $full_method = "${package}::$method";
724              
725             # Capture original implementation before installing the wrapper
726 5         10 my $orig;
727             {
728             ## no critic (ProhibitNoStrict)
729 13     13   97 no strict 'refs';
  13         24  
  13         4004  
  5         6  
730 5         8 $orig = \&{$full_method};
  5         25  
731             }
732              
733             # Install a wrapper that:
734             # - runs the mock once
735             # - restores the original
736             # - delegates all subsequent calls to the original
737             my $wrapper = sub {
738             # Run the mock implementation
739 5     5   163 my @result = $code->(@_);
740              
741             # Restore the previous implementation
742 5         33 Test::Mockingbird::unmock($package, $method);
743              
744             # Return the mock's result
745 5 50       54 return wantarray ? @result : $result[0];
746 5         28 };
747              
748 5         13 local $Test::Mockingbird::TYPE = 'mock_once';
749              
750             # Install the wrapper as a mock layer
751 5         18 return mock $target => $wrapper;
752             }
753              
754             =head2 restore
755              
756             Restore all mock layers for a single method target. This is similar to
757             C<restore_all>, but applies only to one method. If the method was never
758             mocked, this routine has no effect.
759              
760             =head3 API specification
761              
762             =head4 Input (Params::Validate::Strict schema)
763              
764             - C<target>: required, scalar, string; method target in shorthand or longhand form
765              
766             =head4 Output (Returns::Set schema)
767              
768             - C<return>: undef
769              
770             =cut
771              
772             sub restore {
773 5     5 1 8145 my $target = $_[0];
774              
775             # Entry criteria:
776             # - target must be defined
777 5 50       22 croak 'restore requires a target' unless defined $target;
778              
779             # Parse target using existing logic
780 5         20 my ($package, $method) = _parse_target($target);
781 5         16 my $full_method = "${package}::$method";
782              
783             # Exit early if nothing to restore
784 5 100       28 return unless exists $mocked{$full_method};
785              
786             # Restore all layers for this method
787 3         8 while (@{ $mocked{$full_method} }) {
  7         26  
788 4         7 my $prev = pop @{ $mocked{$full_method} };
  4         11  
789              
790 4 50       20 if (defined $prev) {
791             # Restore previous coderef
792 13     13   101 no warnings 'redefine';
  13         30  
  13         722  
793             {
794             ## no critic (ProhibitNoStrict)
795 13     13   74 no strict 'refs';
  13         37  
  13         749  
  4         6  
796 4         7 *{$full_method} = $prev;
  4         39  
797             }
798             } else {
799             # Original method did not exist — remove glob
800             {
801             ## no critic (ProhibitNoStrict)
802 13     13   79 no strict 'refs';
  13         48  
  13         9033  
  0         0  
803 0         0 delete ${"${package}::"}{$method};
  0         0  
804             }
805             }
806             }
807              
808             # Clean up tracking
809 3         8 delete $mocked{$full_method};
810 3         68 delete $mock_meta{$full_method};
811              
812 3         11 return;
813             }
814              
815             =head2 diagnose_mocks
816              
817             Return a structured hashref describing all currently active mock layers.
818             This routine is purely observational and does not modify any state.
819              
820             =head3 API specification
821              
822             =head4 Input
823              
824             Params::Validate::Strict schema:
825              
826             - none
827              
828             =head4 Output
829              
830             Returns::Set schema:
831              
832             - C<return>: hashref; keys are fully qualified method names, values are
833             hashrefs containing:
834             - C<depth>: integer; number of active mock layers
835             - C<layers>: arrayref of hashrefs; each layer has:
836             - C<type>: string
837             - C<installed_at>: string
838             - C<original_existed>: boolean
839              
840             =cut
841              
842             sub diagnose_mocks {
843             # Entry: none
844             # Exit: structured hashref describing all active mocks
845             # Side effects: none
846             # Notes: purely observational
847              
848 5     5 1 3391 my %report;
849              
850 5         24 for my $full_method (sort keys %mocked) {
851             $report{$full_method} = {
852 4         10 depth => scalar @{ $mocked{$full_method} },
853 4   50     185 layers => [ @{ $mock_meta{$full_method} // [] } ],
854 4 50       9 original_existed => defined $mocked{$full_method}[0] ? 1 : 0,
855             };
856             }
857              
858 5         14 return \%report;
859             }
860              
861             =head2 diagnose_mocks_pretty
862              
863             Return a human-readable string describing all currently active mock layers.
864             This routine is purely observational and does not modify any state.
865              
866             =head3 API specification
867              
868             =head4 Input
869              
870             Params::Validate::Strict schema:
871              
872             - none
873              
874             =head4 Output
875              
876             Returns::Set schema:
877              
878             - C<return>: scalar string; formatted multi-line description of all active
879             mock layers, including:
880             - fully qualified method name
881             - depth (number of active layers)
882             - whether the original method existed
883             - each layer's type and installation location
884              
885             =head3 Behaviour
886              
887             =head4 Entry
888              
889             - No arguments are accepted.
890              
891             =head4 Exit
892              
893             - Returns a formatted string describing the current mocking state.
894              
895             =head4 Side effects
896              
897             - None. This routine does not modify C<%mocked>, C<%mock_meta>, or any
898             symbol table entries.
899              
900             =head4 Notes
901              
902             - This routine is intended for debugging and diagnostics. It is safe to
903             call at any point during a test run.
904             - The output format is stable and suitable for human inspection, but not
905             guaranteed to remain fixed for machine parsing.
906              
907             =cut
908              
909             sub diagnose_mocks_pretty {
910             # Entry: none
911             # Exit: formatted string
912             # Side effects: none
913             # Notes: uses diagnose_mocks() internally
914              
915 0     0 1 0 my $diag = diagnose_mocks();
916 0         0 my @out;
917              
918 0         0 for my $full_method (sort keys %$diag) {
919 0         0 my $entry = $diag->{$full_method};
920              
921 0         0 push @out, "$full_method:";
922 0         0 push @out, " depth: $entry->{depth}";
923 0         0 push @out, " original_existed: $entry->{original_existed}";
924              
925 0         0 for my $layer (@{ $entry->{layers} }) {
  0         0  
926             push @out, sprintf(
927             " - type: %-14s installed_at: %s",
928             $layer->{type},
929             $layer->{installed_at},
930 0         0 );
931             }
932              
933 0         0 push @out, '';
934             }
935              
936 0         0 return join "\n", @out;
937             }
938              
939             sub _parse_target {
940 40     40   117 my ($arg1, $arg2, $arg3) = @_;
941              
942             # Shorthand: 'Pkg::method'
943 40 100 66     462 if (defined $arg1 && !defined $arg3 && $arg1 =~ /^(.*)::([^:]+)$/) {
      100        
944 19         105 return ($1, $2);
945             }
946              
947             # Longhand: ('Pkg','method')
948 21         105 return ($arg1, $arg2);
949             }
950              
951             =head1 SUPPORT
952              
953             This module is provided as-is without any warranty.
954              
955             Please report any bugs or feature requests to C<bug-test-mockingbird at rt.cpan.org>,
956             or through the web interface at
957             L<http://rt.cpan.org/NoAuth/ReportBug.html?Queue=Test-Mockingbird>.
958             I will be notified, and then you'll
959             automatically be notified of progress on your bug as I make changes.
960              
961             You can find documentation for this module with the perldoc command.
962              
963             perldoc Test::Mockingbird
964              
965             =head1 AUTHOR
966              
967             Nigel Horne, C<< <njh at nigelhorne.com> >>
968              
969             =head1 BUGS
970              
971             =head1 SEE ALSO
972              
973             =over 4
974              
975             =item * L<Test::Mockingbird::DeepMock>
976              
977             =back
978              
979             =head1 REPOSITORY
980              
981             L<https://github.com/nigelhorne/Test-Mockingbird>
982              
983             =head1 SUPPORT
984              
985             This module is provided as-is without any warranty.
986              
987             =head1 LICENCE AND COPYRIGHT
988              
989             Copyright 2025-2026 Nigel Horne.
990              
991             Usage is subject to licence terms.
992              
993             The licence terms of this software are as follows:
994              
995             =over 4
996              
997             =item * Personal single user, single computer use: GPL2
998              
999             =item * All other users (including Commercial, Charity, Educational, Government)
1000             must apply in writing for a licence for use from Nigel Horne at the
1001             above e-mail.
1002              
1003             =back
1004              
1005             =cut
1006              
1007             1;
1008              
1009             package Test::Mockingbird::Guard;
1010              
1011             sub new {
1012 4     4   10 my ($class, $full_method) = @_;
1013 4         31 return bless { full_method => $full_method }, $class;
1014             }
1015              
1016             sub DESTROY {
1017 4     4   4019 my $self = $_[0];
1018              
1019 4         23 Test::Mockingbird::unmock($self->{full_method});
1020             }
1021              
1022             1;