File Coverage

blib/lib/Data/Trace.pm
Criterion Covered Total %
statement 105 106 99.0
branch 26 28 92.8
condition 9 11 81.8
subroutine 20 20 100.0
pod 1 1 100.0
total 161 166 96.9


line stmt bran cond sub pod time code
1             package Data::Trace;
2              
3             =head1 NAME
4              
5             Data::Trace - Trace when a data structure gets updated.
6              
7             =cut
8              
9 2     2   167302 use 5.006;
  2         10  
10 2     2   16 use strict;
  2         9  
  2         87  
11 2     2   11 use warnings;
  2         4  
  2         129  
12              
13 2     2   1497 use FindBin();
  2         3198  
  2         79  
14 2     2   1263 use lib $FindBin::RealBin;
  2         1847  
  2         13  
15              
16 2     2   1038 use Data::Tie::Watch; # Tie::Watch copy.
  2         7  
  2         74  
17 2     2   1627 use Data::DPath; # All refs in a struct.
  2         247395  
  2         16  
18 2     2   488 use Carp();
  2         4  
  2         50  
19 2     2   10 use parent qw( Exporter );
  2         4  
  2         13  
20 2     2   158 use feature qw( say );
  2         5  
  2         4072  
21              
22             our @EXPORT = qw( Trace );
23             our $VERSION = '1.06';
24              
25             =head1 SYNOPSIS
26              
27             Variable change trace:
28              
29             use Data::Trace;
30              
31             my $data = {a => [0, {complex => 1}]};
32              
33             sub BadCall{ $data->{a}[0] = 1 }
34              
35             Trace($data);
36              
37             BadCall(); # Shows stack trace of where data was changed.
38              
39             Stack trace:
40              
41             use Data::Trace;
42             Trace(); # 1 level.
43             Trace(5); # 5 levels.
44              
45             =cut
46              
47             =head1 DESCRIPTION
48              
49             This module provides a convienient way to find out
50             when a data structure has been updated.
51              
52             It is a debugging/tracing aid for complex systems to identify unintentional
53             alteration to data structures which should be treated as read-only.
54              
55             Probably can also create a variable as read-only in Moose and see where
56             its been changed, but this module is without Moose support.
57              
58             =cut
59              
60             =head1 SUBROUTINES/METHODS
61              
62             =head2 Trace
63              
64             Watch a reference for changes:
65              
66             Trace( \$scalar, @OPTIONS );
67             Trace( \@array , @OPTIONS );
68             Trace( \@hash , @OPTIONS );
69             Trace( $complex_data , @OPTIONS );
70              
71             Just a stack trace with no watching:
72              
73             Trace( @OPTIONS );
74              
75             Options:
76              
77             -clone => 0, # Disable auto tying after a Storable dclone.
78              
79             -var => REF, # Variable to watch.
80             REF # Same as passing a reference.
81              
82             -levels => NUM # How many scope levels to show.
83             NUM # Same as passing a decimal.
84              
85             -raw => 1, # Include Internal call like Moose,
86             # and Class::MOP in a trace.
87             -NUM # Same as passing negative number.
88              
89             -message => STR # Message to use for a normal (non-
90             # tie stack trace).
91             STR # Same as passing anything else.
92              
93             -methods => STR # Monitors only specific methods.
94             -methods => [STR] #
95              
96             =cut
97              
98             sub Trace {
99 103     103 1 252613 my %args = __PACKAGE__->_ProcessArgs( @_ );
100 103 100       323 my $method = $args{-var} ? "_TieNodes" : "_Trace";
101 103         488 __PACKAGE__->$method( %args );
102             }
103              
104             =head2 _ProcessArgs
105              
106             Allows calling Trace like:
107             Trace() and Trace(-levels => 1) to
108             mean the same.
109              
110             =cut
111              
112             sub _ProcessArgs {
113 103     103   397 my ( $class, @raw_args ) = @_;
114 103         209 my %args;
115              
116 103         510 while ( my $arg = shift @raw_args ) {
117 183 100       1137 if ( $arg =~ / ^ - [a-zA-Z_-] /x ) {
    100          
    100          
118 50         209 $args{$arg} = shift @raw_args;
119             }
120             elsif ( ref $arg ) {
121 79         404 $args{-var} = $arg;
122             }
123             elsif ( $arg =~ / ^ (-)? (\d+) $ /x ) {
124 50         221 $args{-levels} = "$2";
125 50 100       288 $args{-raw} = 1 if $1;
126             }
127             else {
128 4         15 $args{-message} = $arg;
129             }
130             }
131              
132 103 100 66     552 $args{-levels} //= ( $args{-var} ? 3 : 1 );
133 103   100     552 $args{-message} //= "HERE:";
134 103   100     481 $args{-raw} //= 0;
135              
136             # Normalize methods and check if valid.
137 103   100     491 my $methods = $args{-methods} //= [];
138 103 100       291 if ( !ref( $methods ) ) {
139 2         7 $methods = $args{-methods} = [$methods];
140             }
141             my %valid_methods =
142 103         287 map { $_ => 1 } $class->_get_valid_methods();
  1648         3897  
143             @$methods =
144 4         15 grep { $valid_methods{$_} }
145 103         441 map { lc } @$methods;
  4         17  
146              
147 103         862 %args;
148             }
149              
150             sub _get_valid_methods {
151 103     103   970 qw(
152             clear
153             delete
154             destroy
155             exists
156             extend
157             fetch
158             fetchsize
159             pop
160             push
161             shift
162             splice
163             store
164             storesize
165             unshift
166             firstkey
167             nextkey
168             );
169             }
170              
171             sub _TieNodes {
172 85     85   402 my ( $class, %args ) = @_;
173              
174 85   50     283 my $var = delete $args{-var} // '';
175 85 50       260 if ( not ref $var ) {
176 0         0 die "Error: trace data must be a reference!";
177             }
178              
179 85         667 my @refs = grep { ref } Data::DPath->match( $var, "//" );
  342         49604  
180 85         707 my %watches = $class->_BuildWatcherMethods( %args );
181 85         271 my @nodes;
182              
183 85         168 for my $ref ( @refs ) {
184 169         1283 push @nodes,
185             Data::Tie::Watch->new(
186             -variable => $ref,
187             %watches,
188             %args,
189             );
190             }
191              
192 85         1911 @nodes;
193             }
194              
195             sub _BuildWatcherMethods {
196 85     85   435 my ( $class, %args ) = @_;
197 85         192 my %watches;
198              
199 85         234 my @methods = $class->_DefineMethodNames();
200 85 100       159 if ( @{ $args{-methods} } ) {
  85         300  
201 4         12 @methods = @{ $args{-methods} };
  4         14  
202             }
203              
204 85         208 for my $name ( @methods ) {
205 733         1363 my $method = ucfirst $name;
206              
207             $watches{"-$name"} = sub {
208              
209             # Process arguments.
210 79     79   209 my ( $_self, @_args ) = @_;
211             my $_args =
212             join ", ",
213 79 50       199 map { defined() ? qq("$_") : "undef" } @_args;
  110         532  
214              
215             # Stack trace.
216 79         801 $class->_Trace( %args, -message => "\U$name\E( $_args ):" );
217              
218             # Run actual method/operation.
219 79         517 $_self->$method( @_args );
220 733         3311 };
221             }
222              
223 85         650 %watches;
224             }
225              
226             sub _DefineMethodNames {
227 85     85   389 qw(
228             store
229             clear
230             delete
231             extend
232             pop
233             push
234             shift
235             splice
236             unshift
237             );
238             }
239              
240             sub _Trace {
241 97     97   405 my ( $class, %args ) = @_;
242 97         228 my @lines;
243             my $counter;
244 97         272 my @trace = $class->_TraceRaw();
245 97 100       445 if ( not $args{-raw} ) {
246 79         270 @trace = $class->_FilterOutInternals( @trace );
247             }
248              
249             # Collect a max amount of lines.
250 97         261 for my $line ( @trace ) {
251 189         339 push @lines, $line;
252 189 100       636 last if ++$counter >= $args{-levels};
253             }
254              
255 97         273 my ( $first, @rest ) = @lines;
256              
257             # Prepend the message.
258             # and prefix to additional lines.
259 97         1330 require Time::Moment;
260 97         5934 my $time = Time::Moment->now->strftime( "%Y/%m/%d-%T%3f" );
261 97         552 @lines = ( "[$time] $args{-message} $first", map { " |- $_" } @rest, );
  92         346  
262              
263             # Add an extra line for visibility.
264 97 100       347 unshift @lines, "" if @lines > 1;
265              
266             # Return the output.
267 97         292 my $output = join "\n", @lines;
268 97 100       283 return $output if defined wantarray;
269              
270             # Or send to STDOUT.
271 88         889 say $output;
272             }
273              
274             sub _TraceRaw {
275 97     97   216 my ( $class ) = @_;
276              
277 97         194 local $Carp::MaxArgNums = -1;
278              
279 97         38496 map { s/ ^ \s+ //xr }
  924         3129  
280             split /\n/,
281             Carp::longmess( $class );
282             }
283              
284             sub _FilterOutInternals {
285 79     79   303 my ( $class, @trace_lines ) = @_;
286              
287             # Stack trace while ignoring specific packages.
288             grep {
289 79         150 !m{
  744         9135  
290              
291             ^ \s* (?:
292             $class
293             | [\w_:]+ :: _wrapped_ \w+
294             | Data::Tie::Watch::callback
295             | Mojolicious
296             | Class::MOP
297             | Try::Tiny
298             | Mojo
299             | eval
300             ) \b
301              
302             |
303              
304             (?:
305             Mojolicious/Controller
306             | Mojolicious
307             | Try/Tiny
308             )
309             \.pm \s+ line
310              
311             }x
312             } @trace_lines;
313             }
314              
315             =head1 AUTHOR
316              
317             Tim Potapov, C<< >>
318              
319             =head1 BUGS
320              
321             Please report any bugs or feature requests to L.
322              
323             Currently only detect C operations.
324             Expand this to also detect C, C, C, etc.
325              
326             =head1 TODO
327              
328             Consider adding an option to have a warn message anytime a structure is FETCHed.
329              
330             =head1 SUPPORT
331              
332             You can find documentation for this module
333             with the perldoc command.
334              
335             perldoc Data::Trace
336              
337             You can also look for information at:
338              
339             L
340              
341             L
342              
343             =head1 LICENSE AND COPYRIGHT
344              
345             This software is Copyright (c) 2024 by Tim Potapov.
346              
347             This is free software, licensed under:
348              
349             The Artistic License 2.0 (GPL Compatible)
350              
351             =cut
352              
353             "\x{1f42a}\x{1f977}"