File Coverage

blib/lib/Spreadsheet/Edit/Log.pm
Criterion Covered Total %
statement 126 140 90.0
branch 23 30 76.6
condition 15 31 48.3
subroutine 31 35 88.5
pod 7 7 100.0
total 202 243 83.1


line stmt bran cond sub pod time code
1             # License: http://creativecommons.org/publicdomain/zero/1.0/
2             # (CC0 or Public Domain). To the extent possible under law, the author,
3             # Jim Avera (email jim.avera at gmail dot com) has waived all copyright and
4             # related or neighboring rights to this document. Attribution is requested
5             # but not required.
6 4     4   1131244 use strict; use warnings FATAL => 'all'; use utf8;
  4     4   17  
  4     4   178  
  4         32  
  4         17  
  4         151  
  4         22  
  4         7  
  4         32  
7 4     4   140 use feature qw(say state lexical_subs current_sub);
  4         7  
  4         398  
8 4     4   25 no warnings qw(experimental::lexical_subs);
  4         7  
  4         338  
9              
10             package Spreadsheet::Edit::Log;
11              
12             # Allow "use <thismodule. VERSION ..." in development sandbox to not bomb
13 4     4   36 { no strict 'refs'; ${__PACKAGE__."::VER"."SION"} = 1999.999; }
  4         9  
  4         468  
14             our $VERSION = '1000.006'; # VERSION from Dist::Zilla::Plugin::OurPkgVersion
15             our $DATE = '2023-09-06'; # DATE from Dist::Zilla::Plugin::OurDate
16              
17 4     4   50 use Carp;
  4         10  
  4         300  
18              
19 4     4   27 use Exporter 5.57 ();
  4         72  
  4         1740  
20             our @EXPORT = qw/fmt_call log_call fmt_methcall log_methcall
21             nearest_call abbrev_call_fn_ln_subname/;
22              
23             sub _btwTN($$@) {
24 5     5   76 my $pfx=shift; my $N=shift; local $_ = join("",@_);
  5         7  
  5         18  
25 5         13 s/\n\z//s;
26 5         63 my ($package, $path, $lno) = caller($N);
27 5         43 (my $fname = $path) =~ s/.*[\\\/]//;
28 5         12 (my $pkg = $package) =~ s/.*:://;
29 5         328 my $s = eval "\"${pfx}\"";
30 5 50       33 confess "ERROR IN btw prefix '$pfx': $@" if $@;
31 5         42 printf "%s %s\n", $s, $_;
32             }
33              
34             sub _genbtw($$) {
35 11     11   65 my ($pkg, $pfx) = @_;
36 11   50 2   57 my $btw = eval{ sub(@) { unshift @_,$pfx,0; goto &_btwTN } } // die $@;
  11         88  
  2         2114  
  2         9  
37 4     4   31 no strict 'refs';
  4         12  
  4         756  
38 11         33 *{"${pkg}::btw"} = \&$btw;
  11         76  
39             }
40             sub _genbtwN($$) {
41 4     4   16 my ($pkg, $pfx) = @_;
42 4   50 3   8 my $btwN = eval{ sub($@) { unshift @_,$pfx; goto &_btwTN } } // die $@;
  4         40  
  3         36  
  3         11  
43 4     4   31 no strict 'refs';
  4         7  
  4         383  
44 4         10 *{"${pkg}::btwN"} = \&$btwN;
  4         1057  
45             }
46             BEGIN {
47 4     4   40 _genbtw(__PACKAGE__,'$lno:');
48 4         14 _genbtwN(__PACKAGE__,'$lno:');
49             }
50              
51             sub import {
52 9     9   2927 my $class = shift;
53 9         23 my $pkg = caller;
54 9         19 my @remaining_args;
55 9         24 foreach(@_) {
56 44 100       156 if (/:btw=(.*)/) { _genbtw($pkg,$1) }
  7 50       34  
57 0         0 elsif (/:btwN=(.*)/) { _genbtwN($pkg,$1) }
58             else {
59 37         66 push @remaining_args, $_;
60             }
61             }
62 9         39 @_ = ($class, @remaining_args);
63 9         165123 goto &Exporter::import
64             }
65              
66             our @EXPORT_OK = qw/btw btwN oops/;
67              
68              
69 4     4   35 use Scalar::Util qw/reftype refaddr blessed weaken/;
  4         27  
  4         305  
70 4     4   27 use List::Util qw/first any all/;
  4         9  
  4         363  
71 4     4   30 use File::Basename qw/dirname basename/;
  4         27  
  4         683  
72              
73 0     0 1 0 sub oops(@) { @_=("\n".(caller)." oops:\n",@_,"\n"); goto &Carp::confess }
  0         0  
74              
75 4     4   39 use Data::Dumper::Interp qw/dvis vis visq avis hvis visnew addrvis u/;
  4         8  
  4         36  
76              
77             my %backup_defaults = (
78             logdest => \*STDERR,
79             is_public_api => sub{ $_[1][3] =~ /(?:::|^)[a-z][^:]*$/ },
80              
81             #fmt_object => sub{ addrvis($_[1]) },
82             # Just show the address, sans class::name. Note addrvis now wraps it in <...>
83             fmt_object => sub{ addrvis(refaddr($_[1])) },
84             );
85              
86             # Return ref to hash of effective options (READ-ONLY).
87             # If the first argument is a hashref it is shifted off and
88             # used as options which override defaults.
89             sub _getoptions {
90 14     14   33 my $pkg;
91 14   33     26 my $N=1; while (($pkg=caller($N)//oops) eq __PACKAGE__) { ++$N }
  14         82  
  0         0  
92 4     4   2334 no strict 'refs';
  4         8  
  4         2274  
93 14         23 my $r = *{$pkg."::SpreadsheetEdit_Log_Options"}{HASH};
  14         80  
94             +{ %backup_defaults,
95             (defined($r) ? %$r : ()),
96 14 100 100     132 ((@_ && ref($_[0]) eq 'HASH') ? %{shift(@_)} : ())
  11 100       60  
97             }
98             }
99              
100             # Format a usually-comma-separated list sans enclosing brackets.
101             #
102             # Items are formatted by vis() and thus strings will be "quoted", except that
103             # \"ref to string" inserts the string value without quotes and suppresses
104             # adjacent commas (for inserting fixed annotations).
105             # Object refs in the top two levels are not visualized.
106             #
107             # If the arguments are recognized as a sequence then they are formatted as
108             # Arg1..ArgN instead of Arg1,Arg2,...,ArgN.
109             #
110             sub _fmt_list($) {
111 11 100   11   44 my @items = ref($_[0]) eq 'ARRAY' ? @{$_[0]} : ($_[0]);
  7         21  
112 11 50       22 oops if wantarray;
113 11 100       35 if (my $is_sequential = (@items >= 4)) {
114 1         3 my $seq;
115 1         3 foreach(@items) {
116 1 50 33     22 $is_sequential=0,last
      0        
      33        
117             unless defined($_) && /^\w+$/ && ($seq//=$items[0])++ eq $_
118             }
119 1 50       5 if ($is_sequential) {
120 0         0 return visq($items[0])."..".visq($items[-1])
121             }
122             }
123             join "", map{
124 11         26 my $item = $items[$_];
  21         18801  
125             ($_ > 0 && (ref($items[$_-1]) ne 'SCALAR' || ${$items[$_-1]} eq "")
126             && (ref($item) ne 'SCALAR' || ${$item} eq "")
127             ? "," : ""
128             )
129 21 100 100     147 .(ref($item) eq 'SCALAR' ? ${$item} : visnew->Pad(" ")->vis($item)
  5 100       15  
130             )
131             } 0..$#items;
132             }
133             ## test
134             #foreach ([], [1..5], ['f'..'i'], ['a'], ['a','x']) {
135             # my @items = @$_;
136             # warn avis(@items)," -> ", scalar(_fmt_list(@items)), "\n";
137             # @items = (\"-FIRST-", @items);
138             # warn avis(@items)," -> ", scalar(_fmt_list(@items)), "\n";
139             # splice @items, int(scalar(@items)/2),0, \"-ANN-" if @items >= 1;
140             # warn avis(@items)," -> ", scalar(_fmt_list(@items)), "\n";
141             # push @items, \"-LAST-";
142             # warn avis(@items)," -> ", scalar(_fmt_list(@items)), "\n";
143             #}
144             #die "TEX";
145              
146             #####################################################################
147             # Locate the nearest call to a public sub in the call stack.
148             #
149             # A callback decides what might be a "public" entrypoint (default:
150             # any sub named starting with [a-z]).
151             #
152             # RETURNS
153             # ([frame], [called args]) in array context
154             # [frame] in scalar context
155             #
156             # "frame" means caller(n) results:
157             # 0 1 2 3
158             # package filename linenum subname ...
159             #
160 4   33     950 use constant _CALLER_OVERRIDE_CHECK_OK =>
161             (defined(&Carp::CALLER_OVERRIDE_CHECK_OK)
162 4     4   38 && &Carp::CALLER_OVERRIDE_CHECK_OK);
  4         9  
163              
164             sub _nearest_call($$) {
165 14     14   30 my ($state, $opts) = @_;
166 14         23 my $callback = $opts->{is_public_api};
167 14         22 for (my $lvl=1 ; ; ++$lvl) {
168 66         385 my @frame = caller($lvl);
169 66 50       143 confess "No public-API sub found" unless defined($frame[0]);
170 66         80 my $calling_pkg = $frame[0];
171 66 50       285 my ($called_pkg) = ($frame[3] =~ /^(.*)::/) or next; # eval?
172 4     4   32 no strict 'refs';
  4         12  
  4         4941  
173             #if ((!any{ $_ eq $called_pkg } (__PACKAGE__,$calling_pkg,@{$calling_pkg."::CARP_NOT"}))
174 66 100 100     216 if ($called_pkg ne __PACKAGE__ && $callback->($state, \@frame)) {
175 14         67 return \@frame;
176             }
177             }
178             }
179             sub nearest_call(;$) {
180 1     1 1 3372 my $opts = &_getoptions;
181 1         4 _nearest_call({}, $opts);
182             }
183              
184             sub _abbrev_call_fn_ln_subname($$) {
185 13     13   31 my @results = @{ &_nearest_call(@_) }[1,2,3]; # (fn, lno, subname)
  13         35  
186 13         298 $results[0] = basename $results[0]; # filename
187 13         72 $results[2] =~ s/.*:://; # subname
188             @results
189 13         45 }
190             sub abbrev_call_fn_ln_subname(;$) {
191 1     1 1 8 my $opts = &_getoptions;
192 1         5 _abbrev_call_fn_ln_subname({},$opts);
193             }
194              
195             sub _fmt_call($;$$) {
196             my $opts = shift;
197             confess "Expecting {optOPTIONS} INPUTS optRESULTS" unless @_==1 or @_==2;
198             my ($inputs, $retvals) = @_;
199             #warn dvis '### $opts\n $inputs\n $retvals';
200              
201             my $state = {};
202             my ($fn, $lno, $subname) = _abbrev_call_fn_ln_subname($state, $opts);
203             my $msg = ">[$fn:$lno] ";
204              
205             my sub myequal($$) {
206             if ((my $r1 = refaddr($_[0])) && (my $r2 = refaddr($_[1]))) {
207             return $r1 == $r2; # same object
208             } else {
209             return u($_[0]) eq u($_[1]); # string reps eq, or both undef
210             }
211             }
212              
213             state $prev_obj;
214             if (defined(my $obj = $opts->{self})) {
215             # N.B. "self" might not be a ref, or might be unblessed
216             if (! myequal($obj, $prev_obj)) {
217             # Show the obj address in only the first of a sequence of calls
218             # with the same object.
219             my $rep = $opts->{fmt_object}->($state, $obj);
220             if (defined($rep) && refaddr($rep)) {
221             $msg .= _fmt_list($rep); # Data::Dumper::Interp
222             } else {
223             $msg .= $rep;
224             }
225             $prev_obj = $obj;
226             weaken($prev_obj);
227             }
228             $msg .= ".";
229             } else {
230             $prev_obj = undef;
231             }
232              
233             $msg .= $subname;
234             $msg .= " "._fmt_list($inputs) if @$inputs;
235             oops "terminal newline in last input item" if substr($msg,-1) eq "\n";
236             if (defined $retvals) {
237             $msg .= "()" if @$inputs == 0;
238             $msg .= " ==> ";
239             $msg .= _fmt_list($retvals);
240             oops "terminal newline in last retvals item" if substr($msg,-1) eq "\n";
241             }
242             $msg."\n"
243             }
244             sub fmt_call {
245 0     0 1 0 my $opts = &_getoptions;
246 0         0 &_fmt_call($opts, @_);
247             }
248              
249             sub log_call {
250 12     12 1 72892 my $opts = &_getoptions;
251 12         32 my $fh = $opts->{logdest};
252 12         76 print $fh &_fmt_call($opts, @_);
253             }
254              
255             sub fmt_methcall($;@) {
256 0     0 1   my $opts = &_getoptions;
257 0   0       my $obj = shift // croak "Missing 'self' argument\n";
258 0           $opts->{self} = $obj;
259 0           &_fmt_call($opts, @_);
260             }
261              
262             sub log_methcall {
263 0     0 1   my $opts = &_getoptions;
264 0           my $fh = $opts->{logdest};
265 0           print $fh &fmt_methcall($opts, @_);
266             }
267              
268             1;
269              
270             __END__
271             =pod
272              
273             =head1 NAME
274              
275             Spreadsheet::Edit::Log - log method/function calls, args, and return values
276              
277             =head1 SYNOPSIS
278              
279             use Spreadsheet::Edit::Log qw/:DEFAULT btw oops/;
280              
281             sub public_method {
282             my $self = shift;
283             $self->_internal_method(@_);
284             }
285             sub _internal_method {
286             my $self = shift;
287              
288             oops "zort not set!" unless defined $self->{zort};
289             btw "By the way, the zort is $self->{zort}" if $self->{debug};
290              
291             my @result = (42, $_[0]*1000);
292              
293             log_call \@_, [\"Here you go:", @result] if $self->{verbose};
294              
295             @result;
296             }
297             ...
298             $obj->public_method(100);
299             # file::lineno public_method 100 ==> Here you go:42,100000
300              
301             =head1 DESCRIPTION
302              
303             (This is generic, no longer specific to Spreadsheet::Edit. Someday it might
304             be published as a stand-alone distribution rather than packaged with
305             Spreadsheet-Edit.)
306              
307             This provides possibly-overkill convenience for "verbose logging" and/or debug
308             tracing of subroutine calls.
309              
310             The resulting message string includes the location of the
311             user's call, the name of the public function or method called,
312             and a representation of the inputs and outputs.
313              
314             The "public" function/method name shown is not necessarily the immediate caller of the logging function.
315              
316             =head2 log_call {OPTIONS}, [INPUTS], [RESULTS]
317              
318             Prints the result of calling C<fmt_call> with the same arguments.
319              
320             The message is written to STDERR unless
321             C<< logdest => FILEHANDLE >> is included in I<OPTIONS>.
322              
323             =head2 $msgstring = fmt_call {OPTIONS}, [INPUTS], [RESULTS]
324              
325             {OPTIONS} and [RESULTS] are optional, i.e. may be entirely omitted.
326              
327             A message string is composed and returned. The general form is:
328              
329             File:linenum funcname input,items,... ==> output,items,...\n
330             or
331             File:linenum Obj<address>->methname input,items,... ==> output,items,...\n
332              
333             C<[INPUTS]> and C<[RESULTS]> are each a ref to an array of items (or
334             a single non-aref item), used to form comma-separated lists.
335              
336             Each item is formatted similar to I<Data::Dumper>, i.e. strings are "quoted"
337             and complex structures serialized; printable Unicode characters are shown as
338             themselves (rather than hex escapes)
339              
340             ... with two exceptions:
341              
342             =over
343              
344             =item 1.
345              
346             If an item is a reference to a string then the string is inserted
347             as-is (unquoted),
348             and unless the string is empty, adjacent commas are suppressed.
349             This allows pasting arbitrary text between values.
350              
351             =item 2.
352              
353             If an item is an object (blessed reference) then only it's type and
354             abbreviated address are shown, unless overridden via
355             the C<fmt_object> option described below.
356              
357             =back
358              
359             B<{OPTIONS}>
360              
361             (See "Default OPTIONS" below to specify most of these statically)
362              
363             =over
364              
365             =item self =E<gt> objref
366              
367             If your sub is a method, your can pass C<self =E<gt> $self> and
368             the the invocant will be displayed separately before the method name.
369             To reduce clutter, the invocant is
370             displayed for only the first of a series of consecutive calls with the
371             same C<self> value.
372              
373             =item fmt_object =E<gt> CODE
374              
375             Format a reference to a blessed thing,
376             or the value of the C<self> option (if passed) whether blessed or not.
377              
378             The sub is called with args ($state, $thing). It should return
379             either C<$thing> or an alternative representation string. By default,
380             the type/classname is shown and an abbreviated address (see C<addrvis>
381             in L<Data::Dumper::Interp>).
382              
383             C<$state> is a ref to a hash where you can store anything you want; it persists
384             only during the current C<fmt_call> invocation.
385              
386             =item is_public_api =E<gt> CODE
387              
388             Recognize a public entry-point in the call stack.
389              
390             The sub is called repeatedly with
391             arguments S<< C<($state, [package,file,line,subname,...])>. >>
392              
393             The second argument contains results from C<caller(N)>.
394             Your sub should return True if the frame represents the call to be described
395             in the message.
396              
397             The default callback is S<<< C<sub{ $_[1][3] =~ /(?:::|^)[a-z][^:]*$/ }> >>>,
398             which looks for any sub named with an initial lower-case letter;
399             in other words, it assumes that internal subs start with an underscore
400             or capital letter (such as for constants).
401              
402             =back
403              
404             =head2 $string = fmt_methcall {OPTIONS}, $self, [INPUTS], [RESULTS]
405              
406             A short-hand for
407              
408             $string = fmt_call {OPTIONS, self => $self}, [INPUTS], [RESULTS]
409              
410             =head2 log_methcall $self, [INPUTS], [RESULTS]
411              
412             =head2 log_methcall {OPTIONS}, $self, [INPUTS], [RESULTS]
413              
414             A short-hand for
415              
416             log_call {OPTIONS, self => $self}, [INPUTS], [RESULTS]
417              
418             Usually {OPTIONS} can be omitted for a more succinct form.
419              
420             =head2 $frame = nearest_call {OPTIONS};
421              
422             Locate the call frame for the "public" interface most recently called.
423             This accesses the internal logic used by C<fmt_call>, and uses the
424             same C<is_public_api> callback.
425              
426             The result is a reference to the items returned by C<caller(N)> which
427             represent the call to be traced.
428              
429             {OPTIONS} may be omitted.
430              
431             =head2 ($filename, $linenum, $subname) = abbrev_call_fn_ln_subname {OPTIONS};
432              
433             Returns abbreviated information from C<nearest_call>, possibly ambiguous
434             but usually more friendly to humans: C<$filename> is the I<basename> only
435             and C<$subname> omits the Package:: prefix.
436              
437             =head2 Default OPTIONS
438              
439             B<our %SpreadsheetEdit_Log_Options = (...);> in your package
440             will be used to override the built-in defaults (but are still
441             overridden by C<{OPTIONS}> passed in individual calls).
442              
443             =head1 Debug Utilities
444              
445             Z<>
446              
447             =head2 btw string,string,...
448              
449             =head2 btwN numlevels,string,string,...
450              
451             For internal debug messages (not related to the other functions).
452              
453             C<btw> prints a message to STDERR preceeded by "linenum:"
454             giving the line number I<of the call to btw>.
455             A newline is appended to the message unless the last string
456             string already ends with a newline.
457              
458             This is like C<warn 'message'> when the message omits a final newline;
459             but with a different presentation.
460              
461             C<btwN> displays the line number of the call <numlevels> earlier
462             in the call stack.
463              
464             Not exported by default.
465              
466             By default messages show only the caller's line number.
467             The special tags B<:btw=PFX> or B<:btwN=PFX> will import a customized function
468             which prefixes messages with the string B<PFX>. This string
469             may contain
470             I<$lno> I<$path> I<$fname> I<$package> or I<$pkg>
471             to interpolate respectively
472             the calling line number, file path, file basename,
473             package name, or S<abbreviated package name (*:: removed).>
474              
475             =head2 oops string,string,...
476              
477             Prepends "\n<your package name> oops:\n" to the message and then
478             chains to Carp::confess for backtrace and death.
479              
480             Not exported by default.
481              
482             =head1 SEE ALSO
483              
484             L<Data::Dumper::Interp>
485              
486             =head1 AUTHOR
487              
488             Jim Avera (jim.avera gmail)
489              
490             =head1 LICENSE
491              
492             Public Domain or CC0
493              
494             =for Pod::Coverage oops
495              
496             =cut
497