File Coverage

blib/lib/Smart/Comments.pm
Criterion Covered Total %
statement 128 148 86.4
branch 32 60 53.3
condition 22 35 62.8
subroutine 18 21 85.7
pod n/a
total 200 264 75.7


line stmt bran cond sub pod time code
1             package Smart::Comments;
2             $Smart::Comments::VERSION = '1.05_02'; # TRIAL
3 22     22   39828 use 5.008;
  22         74  
4 22     22   118 use warnings;
  22         35  
  22         613  
5 22     22   104 use strict;
  22         42  
  22         512  
6 22     22   121 use Carp;
  22         68  
  22         1960  
7              
8 22     22   114 use List::Util qw(sum);
  22         36  
  22         2636  
9              
10 22     22   593579 use Filter::Simple;
  22         2012953  
  22         160  
11              
12             my $maxwidth = 69; # Maximum width of display
13             my $showwidth = 35; # How wide to make the indicator
14             my $showstarttime = 6; # How long before showing time-remaining estimate
15             my $showmaxtime = 10; # Don't start estimate if less than this to go
16             my $whilerate = 30; # Controls the rate at which while indicator grows
17             my $minfillwidth = 5; # Fill area must be at least this wide
18             my $average_over = 5; # Number of time-remaining estimates to average
19             my $minfillreps = 2; # Minimum size of a fill and fill cap indicator
20             my $forupdatequantum = 0.01; # Only update every 1% of elapsed distance
21              
22             # Synonyms for asserts and requirements...
23             my $require = qr/require|ensure|assert|insist/;
24             my $check = qr/check|verify|confirm/;
25              
26             # Horizontal whitespace...
27             my $hws = qr/[^\S\n]/;
28              
29             # Optional colon...
30             my $optcolon = qr/$hws*;?/;
31              
32             # Automagic debugging as well...
33             my $DBX = '$DB::single = $DB::single = 1;';
34              
35             # Implement comments-to-code source filter...
36             FILTER {
37             shift; # Don't need the package name
38             s/\r\n/\n/g; # Handle win32 line endings
39              
40             # Default introducer pattern...
41             my $intro = qr/#{3,}/;
42              
43             # Handle args...
44             my @intros;
45             while (@_) {
46             my $arg = shift @_;
47              
48             if ($arg =~ m{\A -ENV \Z}xms) {
49             my $env = $ENV{Smart_Comments} || $ENV{SMART_COMMENTS}
50             || $ENV{SmartComments} || $ENV{SMARTCOMMENTS}
51             ;
52              
53             return if !$env; # i.e. if no filtering
54              
55             if ($env !~ m{\A \s* 1 \s* \Z}xms) {
56             unshift @_, split m{\s+|\s*:\s*}xms, $env;
57             }
58             }
59             else {
60             push @intros, $arg;
61             }
62             }
63              
64             if (my @unknowns = grep {!/$intro/} @intros) {
65             croak "Incomprehensible arguments: @unknowns\n",
66             "in call to 'use Smart::Comments'";
67             }
68              
69             # Make non-default introducer pattern...
70             if (@intros) {
71             $intro = '(?-x:'.join('|',@intros).')(?!\#)';
72             }
73              
74             # Preserve DATA handle if any...
75             if (s{ ^ __DATA__ \s* $ (.*) \z }{}xms) {
76 22     22   11390 no strict qw< refs >;
  22         55  
  22         15730  
77             my $DATA = $1;
78             open *{caller(1).'::DATA'}, '<', \$DATA or die "Internal error: $!";
79             }
80              
81             # Progress bar on a for loop...
82             s{ ^ $hws* ( (?: [^\W\d]\w*: \s*)? for(?:each)? \s* (?:my)? \s* (?:\$ [^\W\d]\w*)? \s* ) \( ([^;\n]*?) \) \s* \{
83             [ \t]* $intro \s (.*) \s* $
84             }
85             { _decode_for($1, $2, $3) }xgem;
86              
87             # Progress bar on a while loop...
88             s{ ^ $hws* ( (?: [^\W\d]\w*: \s*)? (?:while|until) \s* \( .*? \) \s* ) \{
89             [ \t]* $intro \s (.*) \s* $
90             }
91             { _decode_while($1, $2) }xgem;
92              
93             # Progress bar on a C-style for loop...
94             s{ ^ $hws* ( (?: [^\W\d]\w*: \s*)? for \s* \( .*? ; .*? ; .*? \) \s* ) \{
95             $hws* $intro $hws (.*) $hws* $
96             }
97             { _decode_while($1, $2) }xgem;
98              
99             # Requirements...
100             s{ ^ $hws* $intro [ \t] $require : \s* (.*?) $optcolon $hws* $ }
101             { _decode_assert($1,"fatal") }gemx;
102              
103             # Assertions...
104             s{ ^ $hws* $intro [ \t] $check : \s* (.*?) $optcolon $hws* $ }
105             { _decode_assert($1) }gemx;
106              
107             # Any other smart comment is a simple dump.
108             # Dump a raw scalar (the varname is used as the label)...
109             s{ ^ $hws* $intro [ \t]+ (\$ [\w:]* \w) $optcolon $hws* $ }
110             {Smart::Comments::_Dump(pref=>q{$1:},var=>[$1]);$DBX}gmx;
111              
112             # Dump a labelled scalar...
113             s{ ^ $hws* $intro [ \t] (.+ :) [ \t]* (\$ [\w:]* \w) $optcolon $hws* $ }
114             {Smart::Comments::_Dump(pref=>q{$1},var=>[$2]);$DBX}gmx;
115              
116             # Dump a raw hash or array (the varname is used as the label)...
117             s{ ^ $hws* $intro [ \t]+ ([\@%] [\w:]* \w) $optcolon $hws* $ }
118             {Smart::Comments::_Dump(pref=>q{$1:},var=>[\\$1]);$DBX}gmx;
119              
120             # Dump a labelled hash or array...
121             s{ ^ $hws* $intro [ \t]+ (.+ :) [ \t]* ([\@%] [\w:]* \w) $optcolon $hws* $ }
122             {Smart::Comments::_Dump(pref=>q{$1},var=>[\\$2]);$DBX}gmx;
123              
124             # Dump a labelled expression...
125             s{ ^ $hws* $intro [ \t]+ (.+ :) (.+) }
126             {Smart::Comments::_Dump(pref=>q{$1},var=>[$2]);$DBX}gmx;
127              
128             # Dump an 'in progress' message
129             s{ ^ $hws* $intro $hws* (.+ [.]{3}) $hws* $ }
130             {Smart::Comments::_Dump(pref=>qq{$1});$DBX}gmx;
131              
132             # Dump an unlabelled expression (the expression is used as the label)...
133             s{ ^ $hws* $intro $hws* (.*) $optcolon $hws* $ }
134             {Smart::Comments::_Dump(pref=>q{$1:},var=>Smart::Comments::_quiet_eval(q{[$1]}));$DBX}gmx;
135              
136             # An empty comment dumps an empty line...
137             s{ ^ $hws* $intro [ \t]+ $ }
138             {warn qq{\n};}gmx;
139              
140             # Anything else is a literal string to be printed...
141             s{ ^ $hws* $intro $hws* (.*) }
142             {Smart::Comments::_Dump(pref=>q{$1});$DBX}gmx;
143             };
144              
145             sub _quiet_eval {
146 0     0   0 local $SIG{__WARN__} = sub{};
        0      
147 0         0 return scalar eval shift;
148             }
149              
150 21     21   9136 sub _uniq { my %seen; grep { !$seen{$_}++ } @_ }
  21         41  
  28         126  
151              
152             # Converts an assertion to the equivalent Perl code...
153             sub _decode_assert {
154 21     21   65 my ($assertion, $fatal) = @_;
155              
156             # Choose the right signalling mechanism...
157 21 100       60 $fatal = $fatal ? 'die "\n"' : 'warn "\n"';
158              
159 21         31 my $dump = 'Smart::Comments::_Dump';
160 22     22   131 use Text::Balanced qw(extract_variable extract_multiple);
  22         42  
  22         36112  
161              
162             # Extract variables from assertion and enreference any arrays or hashes...
163 21 50       107 my @vars = map { /^$hws*[%\@]/ ? "$dump(pref=>q{ $_ was:},var=>[\\$_], nonl=>1);"
  28         784  
164             : "$dump(pref=>q{ $_ was:},var=>[$_],nonl=>1);"
165             }
166             _uniq extract_multiple($assertion, [\&extract_variable], undef, 1);
167              
168             # Generate the test-and-report code...
169 21         453 return qq{unless($assertion){warn "\\n", q{### $assertion was not true};@vars; $fatal}};
170             }
171              
172             # Generate progress-bar code for a Perlish for loop...
173             my $ID = 0;
174             sub _decode_for {
175 5     5   31 my ($for, $range, $mesg) = @_;
176              
177             # Give the loop a unique ID...
178 5         10 $ID++;
179              
180             # Rewrite the loop with a progress bar as its first statement...
181 5         154 return "my \$not_first__$ID;$for (my \@SmartComments__range__$ID = $range) { Smart::Comments::_for_progress(qq{$mesg}, \$not_first__$ID, \\\@SmartComments__range__$ID);";
182             }
183              
184             # Generate progress-bar code for a Perlish while loop...
185             sub _decode_while {
186 3     3   18 my ($while, $mesg) = @_;
187              
188             # Give the loop a unique ID...
189 3         7 $ID++;
190              
191             # Rewrite the loop with a progress bar as its first statement...
192 3         45 return "my \$not_first__$ID;$while { Smart::Comments::_while_progress(qq{$mesg}, \\\$not_first__$ID);";
193             }
194              
195             # Generate approximate time descriptions...
196             sub _desc_time {
197 0     0   0 my ($seconds) = @_;
198 0         0 my $hours = int($seconds/3600); $seconds -= 3600*$hours;
  0         0  
199 0         0 my $minutes = int($seconds/60); $seconds -= 60*$minutes;
  0         0  
200 0         0 my $remaining;
201              
202             # Describe hours to the nearest half-hour (and say how close to it)...
203 0 0       0 if ($hours) {
    0          
    0          
204 0 0       0 $remaining =
    0          
    0          
    0          
    0          
205             $minutes < 5 ? "about $hours hour".($hours==1?"":"s")
206             : $minutes < 25 ? "less than $hours.5 hours"
207             : $minutes < 35 ? "about $hours.5 hours"
208             : $minutes < 55 ? "less than ".($hours+1)." hours"
209             : "about ".($hours+1)." hours";
210             }
211             # Describe minutes to the nearest minute
212             elsif ($minutes) {
213 0         0 $remaining = "about $minutes minutes";
214 0 0       0 chop $remaining if $minutes == 1;
215             }
216             # Describe tens of seconds to the nearest ten seconds...
217             elsif ($seconds > 10) {
218 0         0 $seconds = int(($seconds+5)/10);
219 0         0 $remaining = "about ${seconds}0 seconds";
220             }
221             # Never be more accurate than ten seconds...
222             else {
223 0         0 $remaining = "less than 10 seconds";
224             }
225 0         0 return $remaining;
226             }
227              
228             # Update the moving average of a series given the newest measurement...
229             my %started;
230             my %moving;
231             sub _moving_average {
232 29     29   46 my ($context, $next) = @_;
233 29   100     108 my $moving = $moving{$context} ||= [];
234 29         54 push @$moving, $next;
235 29 100       93 if (@$moving >= $average_over) {
236 4         13 splice @$moving, 0, $#$moving-$average_over;
237             }
238 29         162 return sum(@$moving)/@$moving;
239             }
240              
241             # Recognize progress bars...
242             my @progress_pats = (
243             # left extending end marker of bar right
244             # anchor bar ("fill") | gap after bar anchor
245             # ====== ======================= === ================= ====
246             qr{^(\s*.*?) (\[\]\[\]) () \s* (\S?.*)}x,
247             qr{^(\s*.*?) (\(\)\(\)) () \s* (\S?.*)}x,
248             qr{^(\s*.*?) (\{\}\{\}) () \s* (\S?.*)}x,
249             qr{^(\s*.*?) (\<\>\<\>) () \s* (\S?.*)}x,
250             qr{^(\s*.*?) (?>(\S)\2{$minfillreps,}) (\S+) \s{$minfillreps,} (\S.*)}x,
251             qr{^(\s*.*?) (?>(\S)\2{$minfillreps,}) () \s{$minfillreps,} (\S.*)}x,
252             qr{^(\s*.*?) (?>(\S)\2{$minfillreps,}) (\S*) (?=\s*$)}x,
253             qr{^(\s*.*?) () () () \s*$ }x,
254             );
255              
256             # Clean up components of progress bar (inserting defaults)...
257             sub _prog_pat {
258 334     334   568 for my $pat (@progress_pats) {
259 1590 100       7916 $_[0] =~ $pat or next;
260 334   50     3462 return ($1, $2||"", $3||"", $4||"");
      100        
      50        
261             }
262 0         0 return;
263             }
264              
265             # State information for various progress bars...
266             my (%count, %max, %prev_elapsed, %prev_fraction, %showing);
267              
268             # Animate the progress bar of a for loop...
269             sub _for_progress {
270 34     34   21143 my ($mesg, $not_first, $data) = @_;
271 34         53 my ($at, $max, $elapsed, $remaining, $fraction);
272              
273             # Update progress bar...
274 34 100       86 if ($not_first) {
275             # One more iteration towards the maximum...
276 29         76 $at = ++$count{$data};
277 29         60 $max = $max{$data};
278              
279             # How long now (both absolute and relative)...
280 29         63 $elapsed = time - $started{$data};
281 29 50       84 $fraction = $max>0 ? $at/$max : 1;
282              
283             # How much change occurred...
284 29         60 my $motion = $fraction - $prev_fraction{$data};
285              
286             # Don't update if count wrapped (unlikely) or if finished
287             # or if no visible change...
288 29 50 66     229 return unless $not_first < 0
      66        
289             || $at == $max
290             || $motion > $forupdatequantum;
291              
292             # Guestimate how long still to go...
293 29 50       124 $remaining = _moving_average $data,
294             $fraction ? $elapsed/$fraction-$elapsed
295             : 0;
296             }
297             # If first iteration...
298             else {
299             # Start at the beginning...
300 5         24 $at = $count{$data} = 0;
301              
302             # Work out where the end will be...
303 5         15 $max = $max{$data} = $#$data;
304              
305             # Start the clock...
306 5         42 $started{$data} = time;
307 5         8 $elapsed = 0;
308 5         10 $fraction = 0;
309              
310             # After which, it will no longer be the first iteration.
311 5         12 $_[1] = 1; # $not_first
312             }
313              
314             # Remember the previous increment fraction...
315 34         83 $prev_fraction{$data} = $fraction;
316              
317             # Now draw the progress bar (if it's a valid one)...
318 34 50       79 if (my ($left, $fill, $leader, $right) = _prog_pat($mesg)) {
319             # Insert the percentage progress in place of a '%'...
320 34         142 s/%/int(100*$fraction).'%'/ge for ($left, $leader, $right);
  10         40  
321              
322             # Work out how much space is available for the bar itself...
323 34         67 my $fillwidth = $showwidth - length($left) - length($right);
324              
325             # But no less than the prespecified minimum please...
326 34 50       84 $fillwidth = $minfillwidth if $fillwidth < $minfillwidth;
327              
328             # Make enough filler...
329 34         79 my $totalfill = $fill x $fillwidth;
330              
331             # How big is the end of the bar...
332 34         50 my $leaderwidth = length($leader);
333              
334             # Truncate where?
335 34 100       93 my $fillend = $at==$max ? $fillwidth
336             : $fillwidth*$fraction-$leaderwidth;
337 34 100       96 $fillend = 0 if $fillend < 0;
338              
339             # Now draw the bar, using carriage returns to overwrite it...
340 34         267 print STDERR "\r", " "x$maxwidth,
341             "\r", $left,
342             sprintf("%-${fillwidth}s",
343             substr($totalfill, 0, $fillend)
344             . $leader),
345             $right;
346              
347             # Work out whether to show an ETA estimate...
348 34 0 33     103 if ($elapsed >= $showstarttime &&
      0        
      33        
349             $at < $max &&
350             ($showing{$data} || $remaining && $remaining >= $showmaxtime)
351             ) {
352 0         0 print STDERR " (", _desc_time($remaining), " remaining)";
353 0         0 $showing{$data} = 1;
354             }
355              
356             # Close off the line, if we're finished...
357 34 100       170 print STDERR "\r", " "x$maxwidth, "\n" if $at >= $max;
358             }
359             }
360              
361             my %shown;
362             my $prev_length = -1;
363              
364             # Animate the progress bar of a while loop...
365             sub _while_progress {
366 300     300   5571 my ($mesg, $not_first_ref) = @_;
367 300         367 my $at;
368              
369             # If we've looped this one before, recover the current iteration count...
370 300 100       595 if ($$not_first_ref) {
371 297         716 $at = ++$count{$not_first_ref};
372             }
373             # Otherwise set the iteration count to zero...
374             else {
375 3         12 $at = $count{$not_first_ref} = 0;
376 3         8 $$not_first_ref = 1;
377             }
378              
379             # Extract the components of the progress bar...
380 300 50       595 if (my ($left, $fill, $leader, $right) = _prog_pat($mesg)) {
381             # Replace any '%' with the current iteration count...
382 300         1134 s/%/$at/ge for ($left, $leader, $right);
  200         936  
383              
384             # How much space is there for the progress bar?
385 300         539 my $fillwidth = $showwidth - length($left) - length($right);
386              
387             # Make it at least the prespecified minimum amount...
388 300 50       650 $fillwidth = $minfillwidth if $fillwidth < $minfillwidth;
389              
390             # How big is the end of the bar?
391 300         386 my $leaderwidth = length($leader);
392              
393             # How big does that make the bar itself (use reciprocal growth)...
394 300         716 my $length = int(($fillwidth-$leaderwidth)
395             *(1-$whilerate/($whilerate+$at)));
396              
397             # Don't update if the picture would look the same...
398             return
399 300 100 66     1807 if length $fill && $prev_length == $length;
400              
401             # Otherwise, remember where we got to...
402 34         46 $prev_length = $length;
403              
404             # And print the bar...
405 34         294 print STDERR "\r", " "x$maxwidth,
406             "\r", $left,
407             sprintf("%-${fillwidth}s", substr($fill x $fillwidth, 0, $length) . $leader),
408             $right;
409             }
410             }
411              
412             # Vestigal (I think)...
413             #sub Assert {
414             # my %arg = @_;
415             # return unless $arg{pass}
416             #}
417              
418 22     22   24299 use Data::Dumper 'Dumper';
  22         168380  
  22         14696  
419              
420             # Dump a variable and then reformat the resulting string more prettily...
421             my $prev_STDOUT = 0;
422             my $prev_STDERR = 0;
423             my %prev_caller = ( file => q{}, line => 0 );
424              
425             sub _Dump {
426 38     38   31575 my %args = @_;
427 38         112 my ($pref, $varref, $nonl) = @args{qw(pref var nonl)};
428              
429             # Handle timestamps and spacestamps...
430 38         114 my (undef, $file, $line) = caller;
431 38         82 $pref =~ s/<(?:now|time|when)>/scalar localtime()/ge;
  0         0  
432 38         67 $pref =~ s/<(?:here|place|where)>/"$file", line $line/g;
433 38         77 $pref =~ s/<(?:file)>/$file/g;
434 38         66 $pref =~ s/<(?:line)>/$line/g;
435              
436             # Add a newline?
437 38         110 my @caller = caller;
438             my $spacer_required
439             = $prev_STDOUT != tell(*STDOUT)
440             || $prev_STDERR != tell(*STDERR)
441             || $prev_caller{file} ne $caller[1]
442 38   100     481 || $prev_caller{line} != $caller[2]-1;
443 38   100     160 $spacer_required &&= !$nonl;
444 38         108 @prev_caller{qw} = @caller[1,2];
445              
446             # Handle a prefix with no actual variable...
447 38 100 66     200 if ($pref && !defined $varref) {
448 11         23 $pref =~ s/:$//;
449 11 100       43 print STDERR "\n" if $spacer_required;
450 11         43 warn "### $pref\n";
451 11         21 $prev_STDOUT = tell(*STDOUT);
452 11         19 $prev_STDERR = tell(*STDERR);
453 11         42 return;
454             }
455              
456             # Set Data::Dumper up for a tidy dump and do the dump...
457 27         46 local $Data::Dumper::Quotekeys = 0;
458 27         43 local $Data::Dumper::Sortkeys = 1;
459 27         42 local $Data::Dumper::Indent = 2;
460 27         89 my $dumped = Dumper $varref;
461              
462             # Clean up the results...
463 27         1812 $dumped =~ s/\$VAR1 = \[\n//;
464 27         228 $dumped =~ s/\s*\];\s*$//;
465 27         83 $dumped =~ s/\A(\s*)//;
466              
467             # How much to shave off and put back on each line...
468 27         73 my $indent = length $1;
469 27         75 my $outdent = " " x (length($pref) + 1);
470              
471             # Report "inside-out" and "flyweight" objects more cleanly...
472 27         60 $dumped =~ s{bless[(] do[{]\\[(]my \$o = undef[)][}], '([^']+)' [)]}
473             {}g;
474              
475             # Adjust the indents...
476 27         298 $dumped =~ s/^[ ]{$indent}([ ]*)/### $outdent$1/gm;
477              
478             # Print the message...
479 27 100       84 print STDERR "\n" if $spacer_required;
480 27         103 warn "### $pref $dumped\n";
481 27         56 $prev_STDERR = tell(*STDERR);
482 27         125 $prev_STDOUT = tell(*STDOUT);
483             }
484              
485             1; # Magic true value required at end of module
486             __END__