File Coverage

blib/lib/Spreadsheet/Edit/Log.pm
Criterion Covered Total %
statement 125 140 89.2
branch 23 30 76.6
condition 15 31 48.3
subroutine 29 33 87.8
pod 7 7 100.0
total 199 241 82.5


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