File Coverage

blib/lib/Test/Simpler.pm
Criterion Covered Total %
statement 205 207 99.0
branch 63 70 90.0
condition 19 24 79.1
subroutine 31 31 100.0
pod 1 1 100.0
total 319 333 95.8


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