File Coverage

blib/lib/Test/Simpler.pm
Criterion Covered Total %
statement 219 220 99.5
branch 64 70 91.4
condition 19 24 79.1
subroutine 35 35 100.0
pod 1 1 100.0
total 338 350 96.5


line stmt bran cond sub pod time code
1             package Test::Simpler;
2              
3 3     3   162762 use warnings;
  3         10  
  3         191  
4 3     3   16 use strict;
  3         7  
  3         104  
5 3     3   5915 use autodie;
  3         86875  
  3         22  
6 3     3   26457 use 5.014;
  3         31  
  3         226  
7              
8             our $VERSION = '0.000007';
9              
10 3     3   3190 use PadWalker qw< peek_my peek_our >;
  3         2721  
  3         277  
11 3     3   2760 use Data::Dump qw< dump >;
  3         31624  
  3         266  
12 3     3   32 use List::Util qw< max >;
  3         8  
  3         313  
13              
14 3     3   17 use base 'Test::Builder::Module';
  3         5  
  3         1977  
15              
16             # Export the module's interface...
17             our @EXPORT = ( 'ok' );
18             our @EXPORT_OK = ();
19             our %EXPORT_TAGS = ();
20              
21             sub ok($;$) {
22 12     12 1 15100 my $outcome = shift;
23 12 100       66 my $desc = @_ ? "@_" : undef;
24              
25             # Grab the upscope variables...
26 12         27 my %value_for = ( %{peek_our(1)}, %{peek_my(1)} );
  12         143  
  12         105  
27              
28             # Cache for source code...
29 12         52 state %source;
30              
31             # Where were we called???
32 12         52 my ($package, $file, $line) = caller;
33              
34             # Grab the source...
35 12 100       85 if (!exists $source{$file}) {
36 2         15 open my $fh, '<', $file;
37 2         9214 $source{$file} = do { local $/; readline $fh };
  2         14  
  2         78  
38             }
39 12         33 my $source = $source{$file};
40 12         29 my $remove_lines = $line - 1;
41 12         1176 $source =~ s{ \A (?: \N*\n ){$remove_lines} }{}xms;
42              
43             # Extract code from source...
44 3     3   20526 use PPI;
  3         556049  
  3         6462  
45 12         125 my $doc = PPI::Document->new(\$source);
46              
47             # Extract statement from code...
48 12         354406 my @target;
49 12         76 STATEMENT:
50 12         39 for my $statement (@{ $doc->find('PPI::Statement') }) {
51 12         93107 my @token = $statement->children;
52 12 50       187 next STATEMENT if $token[0]->content ne 'ok';
53 12         161 @target = @token[1..$#token]; # don't need the 'ok'
54 12         58 last STATEMENT;
55             }
56              
57             # Did we find the statement?
58 12 50       81 die "Can't understand arguments to ok()" if !@target;
59              
60             # Flatten to a list of relevant tokens...
61             SKIPPED:
62 12         24 while (1) {
63             # Remove whitespaces...
64 30 100       353 if ($target[0]->isa('PPI::Token::Whitespace')) {
    100          
    100          
65 14         80 shift @target;
66             }
67             # Step into lists...
68             elsif ($target[0]->isa('PPI::Structure::List')) {
69 2         46 @target = $target[0]->children;
70             }
71             # Step into expressions...
72             elsif ($target[0]->isa('PPI::Statement::Expression')) {
73 2         10 @target = $target[0]->children;
74             }
75             else {
76 12         28 last SKIPPED;
77             }
78             }
79              
80             # Find first comma or end-of-statement (i.e. end of first arg)...
81             TOKEN:
82 12         54 for my $n (0..$#target) {
83 93         246 my $target = $target[$n];
84              
85             # The comma is an operator...
86 93 100 66     885 if ($target->isa('PPI::Token::Operator')
87             || $target->isa('PPI::Token::Structure')) {
88             # But is the operator the one we want???
89 24         167 my $content = $target->content;
90 24 100       237 if ($content =~ m{^(?: , | => | ; )$}x) {
91             # IF so, truncate tokens here and escape...
92 8         25 splice @target, $n;
93 8         22 last TOKEN;
94             }
95             }
96             }
97              
98             # Compact and clean up the resulting code...
99 12         61 my $test_code = _rebuild_code(@target);
100              
101             # Split on a comparison operator...
102 12         40 state $COMPARATOR
103             = qr{\A(?:
104             eq | ne | lt | le | gt | ge
105             | == | != | < | <= | > | >=
106             | =~ | !~ | ~~
107             ) \Z }x;
108              
109 12         30 my $expected_code = $test_code;
110 12         24 my ($got_code, $comparator);
111 12         36 for my $n (0..$#target) {
112 85         258 my $target = $target[$n]->content;
113              
114             # Find a comparison operator to split upon...
115 85 100       1079 if ($target =~ $COMPARATOR) {
116 10         51 $got_code = _rebuild_code(@target[0..$n-1]);
117 10         27 $comparator = $target;
118 10         40 $expected_code = _rebuild_code(@target[$n+1..$#target]);
119             }
120             }
121              
122              
123 12   66     76 $desc //= $test_code;
124              
125             # Extract all the variables from the code...
126 12         32 my @symbols = _uniq( map { _get_symbols($_) } @target );
  85         168  
127              
128 12         174 my @symbol_names;
129             my @symbol_lookup;
130              
131 12         27 for my $symbol (@symbols) {
132 15         23 my $subscript;
133 15         43 my $symbol_source = $symbol->content;
134 15         70 my $next_symbol = $symbol;
135              
136             ACCUMULATE_SYMBOL:
137 15         91 while ($next_symbol = $next_symbol->snext_sibling) {
138             # A simple array or hash look-up???
139 27 100       1112 if ($next_symbol->isa('PPI::Structure::Subscript')) {
    100          
140 8         26 $subscript .= $next_symbol->content;
141 8         245 $symbol_source .= $next_symbol->content;
142             }
143              
144             # A dereferenced look-up or method call???
145             elsif ($next_symbol->content eq '->') {
146             # What's after the arrow???
147 4         45 $next_symbol = $next_symbol->snext_sibling;
148              
149             # Is it a subscript??? Then deal with it on the next loop...
150 4 100 66     108 if ($next_symbol->isa('PPI::Structure::Subscript')) {
    50          
151 2         6 redo ACCUMULATE_SYMBOL;
152             }
153              
154             # Is it a method call??? Then deal with it here...
155             elsif ($next_symbol->isa('PPI::Token::Word') || $next_symbol->isa('PPI::Token::Symbol') ) {
156 2         7 my $methname = $next_symbol->content;
157 2 100 66     26 if ($next_symbol->isa('PPI::Token::Symbol') && $value_for{$next_symbol->content}) {
158 1         13 $methname = ${ $value_for{$next_symbol->content} }
  1         3  
159             }
160              
161             # Save the arrow and method name...
162 2         10 $subscript .= '->' . $methname;
163 2         9 $symbol_source .= '->' . $next_symbol->content;
164              
165             # Look for a trailing argument list...
166 2         21 $next_symbol = $next_symbol->snext_sibling;
167              
168             # Ignore this symbol if it's not a list...
169             redo ACCUMULATE_SYMBOL
170 2 100       61 if ! $next_symbol->isa('PPI::Structure::List');
171              
172             # Otherwise, keep the list and continue...
173 1         13 $subscript .= $next_symbol->content;
174 1         20 $symbol_source .= $next_symbol->content;
175             }
176             }
177             else {
178 15         109 last ACCUMULATE_SYMBOL;
179             }
180             }
181 15         91 my $symbol_name = $symbol->symbol;
182 15 100       1124 my $symbol_lookup = $symbol->symbol_type eq '$'
183             ? '${$value_for{q{' . $symbol_name . '}}}'
184             : '$value_for{q{' . $symbol_name . '}}'
185             ;
186              
187 15 100       1277 if (length $subscript) {
188 6         25 $subscript =~ s{\A->}{}xms;
189 6         19 $symbol_lookup .= "->$subscript";
190             }
191              
192 15         33 push @symbol_names, $symbol_source;
193 15         42 push @symbol_lookup, $symbol_lookup;
194             }
195              
196 12         31 my $symlen = max map { length $_ } @symbol_names;
  15         79  
197              
198             # Now report the test...
199 12         34 local $Test::Builder::Level = $Test::Builder::Level + 1;
200 12         99 my $builder = Test::Builder->new;
201              
202 12         145 $builder->no_diag(1);
203 12         185 $builder->ok($outcome, $desc);
204 12         38271 $builder->no_diag(0);
205              
206             # And report the problem (if any)...
207 12 100       171 if (!$outcome) {
208 7         17 state $VAR_FORMAT = q{ %-*s --> %s};
209 7         40 $builder->diag(" Failed test at $file line $line");
210 7 100       1250 $builder->diag(" $got_code") if defined $got_code;
211 7 100       1270 $builder->diag(" isn't $comparator") if defined $comparator;
212 7 100       939 if (defined $comparator) {
213 6         29 $builder->diag(" $expected_code");
214 6         662 $builder->diag(" Because:");
215             }
216             else {
217 1         45 $builder->diag(" Expected true value for: $expected_code");
218 1         82 $builder->diag(" But was false because:");
219             }
220 7 100       655 if (@symbol_names) {
221 6         18 for my $symbol ( @symbol_names ) {
222 9         1051 my $symbol_lookup = shift @symbol_lookup;
223 9     1   2512 $builder->diag(
  1     1   10  
  1     1   2  
  1     1   63  
  1     1   7  
  1     1   4  
  1     1   50  
  1     1   10  
  1     1   4  
  1         68  
  1         15  
  1         3  
  1         104  
  1         7  
  1         2  
  1         44  
  1         11  
  1         3  
  1         65  
  1         13  
  1         3  
  1         89  
  1         11  
  1         5  
  1         86  
  1         6  
  1         2  
  1         39  
224             sprintf $VAR_FORMAT, $symlen, $symbol,
225             _tidy_values(eval "package $package; no warnings; $symbol_lookup")
226             );
227             }
228 6         1700 $builder->diag(q{});
229             }
230 7 100       603 if (defined $got_code) {
231 6     1   515 my $got_code_value = eval "package $package; no warnings; $got_code";
  1     1   6  
  1     1   3  
  1     1   51  
  1     1   6  
  1     1   2  
  1         48  
  1         7  
  1         2  
  1         52  
  1         10  
  1         2  
  1         53  
  1         6  
  1         2  
  1         51  
  1         5  
  1         3  
  1         52  
232 6     1   444 my $expected_code_value = eval "package $package; no warnings; $expected_code";
  1     1   6  
  1     1   2  
  1     1   46  
  1     1   5  
  1     1   3  
  1         21  
  1         6  
  1         2  
  1         36  
  1         5  
  1         2  
  1         21  
  1         7  
  1         2  
  1         26  
  1         5  
  1         3  
  1         22  
233 6 50       25 my $symlen = max map { defined $_ ? length $_ : 0 } $got_code, $expected_code;
  12         60  
234 6 100 100     72 if (defined( $got_code_value // $expected_code_value ) && !@symbol_names) {
      100        
235 1         4 $builder->diag(" because:");
236             }
237 6 100 66     100 if (defined $got_code_value && $got_code_value ne $got_code) {
238 1         9 $builder->diag( sprintf $VAR_FORMAT, $symlen, $got_code, $got_code_value);
239             }
240 6 100 100     154 if (defined $expected_code_value && $expected_code_value ne $expected_code) {
241 2         13 $builder->diag( sprintf $VAR_FORMAT, $symlen, $expected_code, $expected_code_value);
242             }
243             }
244             }
245             }
246              
247             sub _rebuild_code {
248 32     32   67 my $code = join q{}, map { my $content = $_;
  156         2413  
249 156 100       393 $content =~ /^\n+/ ? q{}
    100          
250             : $content =~ /^\s*$/ ? q{ }
251             : $_
252             } @_;
253 32         1575 return $code =~ s{\A\s+|\s+\Z}{}gr;
254             }
255              
256             sub _tidy_values {
257 9     9   112 my ($ref) = @_;
258              
259 9         18 my $type = ref($ref);
260              
261 2         11 return $type eq 'ARRAY' ? dump @{$ref}
  0         0  
262             : $type eq 'HASH' ? dump($ref) =~ s/^{/(/r =~ s/}$/)/r
263 9 50       85 : $type eq 'SCALAR' ? dump ${$ref}
    50          
    100          
264             : dump $ref;
265             }
266              
267             sub _get_symbols {
268 135     135   149 my $element = shift;
269 135 100       772 return $element if $element->isa('PPI::Token::Symbol');
270 118         205 return map { _get_symbols($_) } eval{ $element->children };
  50         200  
  118         1121  
271             }
272              
273             sub _uniq {
274 12     12   23 my %seen;
275 12 100       30 return grep { $seen{$_}++ ? () : $_ } @_;
  17         108  
276             }
277              
278             1; # Magic true value required at end of module
279             __END__