File Coverage

blib/lib/TAP/Spec/Parser.pm
Criterion Covered Total %
statement 138 190 72.6
branch 30 44 68.1
condition 4 12 33.3
subroutine 42 58 72.4
pod n/a
total 214 304 70.3


line stmt bran cond sub pod time code
1             package TAP::Spec::Parser;
2             BEGIN {
3 2     2   861 $TAP::Spec::Parser::AUTHORITY = 'cpan:ARODLAND';
4             }
5             {
6             $TAP::Spec::Parser::VERSION = '0.07_991'; # TRIAL
7             }
8             # ABSTRACT: Reference implementation of the TAP specification
9 2     2   791 use Mouse;
  2         38094  
  2         5  
10 2     2   2001 use Method::Signatures::Simple;
  2         34627  
  2         10  
11 2     2   645 use Try::Tiny;
  2         2  
  2         85  
12 2     2   760 use Marpa::R2;
  2         220292  
  2         86  
13 2     2   765 use TAP::Spec::TestSet ();
  2         119  
  2         96  
14              
15             has 'exhaustive_strings' => (
16             isa => 'Int',
17             is => 'ro',
18             default => 0,
19             );
20              
21             has 'reader' => (
22             isa => 'CodeRef',
23             is => 'ro',
24             required => 1,
25             );
26              
27              
28             # API adapters to MGC
29 2     2   755 method new_from_string ($class: $string, %args) {
  5     5   8  
  5         8  
  5         7  
30 2 50   2   15 open my $fh, '<', \$string or die $!;
  2         3  
  2         12  
  5         113  
31             my $reader = sub {
32 20     20   754 scalar <$fh>;
33 5         1760 };
34              
35 5         90 $class->new(%args, reader => $reader);
36             }
37              
38 2     2   643 method parse_from_string ($class: $string, %args) {
  5     5   4074  
  5         12  
  5         7  
39 5         20 $class->new_from_string($string, %args)->parse;
40             }
41              
42              
43 2     2   526 method new_from_handle ($class: $handle, %args) {
  0     0   0  
  0         0  
  0         0  
44             my $reader = sub {
45 0     0   0 scalar <$handle>;
46 0         0 };
47              
48 0         0 $class->new(%args, reader => $reader);
49             }
50              
51 2     2   546 method parse_from_handle ($class: $handle, %args) {
  0     0   0  
  0         0  
  0         0  
52 0         0 $class->new_from_handle($handle, %args)->parse;
53             }
54              
55              
56 2     2   494 method new_from_file ($class: $file, %args) {
  0     0   0  
  0         0  
  0         0  
57 0 0       0 open my $fh, '<', $file or die $!;
58             my $reader = sub {
59 0     0   0 scalar <$fh>;
60 0         0 };
61              
62 0         0 $class->new(%args, reader => $reader);
63             }
64              
65 2     2   571 method parse_from_file ($class: $file, %args) {
  0     0   0  
  0         0  
  0         0  
66 0         0 $class->new_from_file($file, %args)->parse;
67             }
68              
69             my $stream_grammar = Marpa::R2::Grammar->new({
70             actions => 'TAP::Spec::Parser::Actions',
71             start => 'Testset',
72             rules => q{
73             # Testset = Header (Plan Body / Body Plan) Footer
74             Testset ::= Header Plan_And_Body Footer EOF action => Testset
75             Plan_And_Body ::=
76             Plan Body action => Plan_Body
77             | Body Plan action => Body_Plan
78              
79             # Header = [Comments] [Version]
80             Header ::= Maybe_Comments Maybe_Version action => Header
81             Maybe_Comments ::= Comments action => subrule1
82             Maybe_Comments ::= action => undef
83             Maybe_Version ::= Version action => subrule1
84             Maybe_Version ::= action => undef
85              
86             # Footer = [Comments]
87             Footer ::= Maybe_Comments action => Footer
88              
89             # Body = *(Comment / TAP-Line)
90             Body ::= Body_Line* action => Body
91             Body_Line ::=
92             Comment action => subrule1
93             | TAP_Line action => subrule1
94              
95             # Comments = 1*Comment
96             Comments ::= Comment+ action => Comments
97             },
98             });
99             $stream_grammar->precompute;
100              
101 2     2   545 method stream_grammar {
  5     5   5  
  5         6  
102 5         45 $stream_grammar
103             }
104              
105             my $line_grammar = Marpa::R2::Grammar->new({
106             actions => 'TAP::Spec::Parser::Actions',
107             start => 'Valid_Line',
108             rules => q{
109             # "Any output line that is not a version, a plan, a test line, a diagnostic
110             # or a bail out is considered an 'unknown' line."
111             # Valid_Line is a meta-rule that matches any valid line of TAP (a rule that
112             # starts at the beginning of a line and matches EOL at the end). Any line of
113             # input that doesn't match "Valid_Line" is discarded as a "junk line", so
114             # keep this up to date.
115             Valid_Line ::=
116             TAP_Line action => tokenize_TAP_Line
117             | Version action => tokenize_Version
118             | Plan action => tokenize_Plan
119             | Comment action => tokenize_Comment
120              
121             # Tap-Line = Test-Result / Bail-Out
122             TAP_Line ::=
123             Test_Result action => subrule1
124             | Bail_Out action => subrule1
125              
126             # Version = "TAP version" SP Version-Number EOL ; ie. "TAP version 13"
127             Version ::= TAP_version SP Version_Number EOL action => Version
128              
129             # Version-Number = Positive-Integer
130             Version_Number ::= Positive_Integer action => subrule1
131              
132             # Plan = ( Plan-Simple / Plan-Todo / Plan-Skip-All ) EOL
133             Plan ::=
134             Plan_Simple EOL action => subrule1
135             | Plan_Todo EOL action => subrule1
136             | Plan_Skip_All EOL action => subrule1
137              
138             # Plan-Simple = "1.." Number-Of-Tests
139             Plan_Simple ::= Plan_Simple_Body action => Plan_Simple
140             Plan_Simple_Body ::= ONE_DOT_DOT Number_Of_Tests action => subrule2 # Capture no. of tests
141              
142             # Plan-Todo = Plan-Simple "todo" 1*(SP Test-Number) ";" ; obsolete
143             Plan_Todo ::= Plan_Simple_Body SP todo SP Test_Numbers SEMI action => Plan_Todo
144             Test_Numbers ::= Test_Number+ separator => SP proper => 1 action => Test_Numbers
145              
146             # Plan-Skip-All = "1..0" SP "skip" SP Reason
147             Plan_Skip_All ::= ONE_DOT_DOT_0 SP skip SP Reason action => Plan_Skip_All
148              
149             # Reason = String
150             Reason ::= String action => subrule1
151              
152             # Test-Number = Positive-Integer
153             Test_Number ::= Positive_Integer action => subrule1
154              
155             # Test-Result = Status [SP Test-Number] [SP Description]
156             # [SP "#" SP Directive [SP Reason]] EOL
157             Test_Result ::= Status Maybe_Test_Number Maybe_Description Maybe_Directive_Reason EOL action => Test_Result
158             Maybe_Test_Number ::= SP Test_Number action => subrule2
159             Maybe_Test_Number ::= action => undef
160             Maybe_Description ::= SP Description action => subrule2
161             Maybe_Description ::= action => undef
162             Maybe_Directive_Reason ::= SP HASH SP Directive Maybe_Reason action => Maybe_Directive_Reason
163             Maybe_Directive_Reason ::= action => undef
164             Maybe_Reason ::= SP Reason action => subrule2
165             Maybe_Reason ::= action => undef
166              
167             # Status = "ok" / "not ok"
168             Status ::=
169             ok action => subrule1
170             | not_ok action => subrule1
171              
172             # Description = Safe-String
173             Description ::= Safe_String action => subrule1
174              
175             # Directive = "SKIP" / "TODO"
176             Directive ::=
177             SKIP action => subrule1
178             | TODO action => subrule1
179              
180             # Bail-Out = "Bail out!" [SP Reason] EOL
181             Bail_Out ::= Bail_out Maybe_Reason EOL action => Bail_Out
182              
183             # Comment = "#" String EOL
184             Comment ::= HASH String EOL action => Comment
185              
186             # String = 1*(Safe-String / "#")
187             String ::= String_Part+ action => String
188             String_Part ::=
189             Safe_String action => subrule1
190             | HASH action => subrule1
191             },
192             });
193             $line_grammar->precompute;
194              
195 2     2   526 method line_grammar {
  16     16   15  
  16         12  
196 16         74 $line_grammar
197             }
198              
199             my %tokens = (
200             'ONE_DOT_DOT' => [ qr/\G1\.\./ ],
201             'ONE_DOT_DOT_0' => [ qr/\G1\.\.0/ ],
202             'TODO' => [ qr/\GTODO/i, 'TODO' ],
203             'SKIP' => [ qr/\GSKIP/i, 'SKIP' ],
204             'ok' => [ qr/\Gok/i, 'ok' ],
205             'not_ok' => [ qr/\Gnot ok/i, 'not ok' ],
206             'TAP_version' => [ qr/\GTAP version/i ],
207             'Bail_out' => [ qr/\GBail out!/i ],
208             'HASH' => [ qr/\G#/, '#' ],
209             'SEMI' => [ qr/\G;/, ';' ],
210             'SP' => [ qr/\G /, ' ' ],
211            
212             # EOL = LF / CRLF
213             'EOL' => [ qr/\G(?:\n|\r\n)/ ],
214            
215             # Safe-String = 1*(%x01-09 %x0B-0C %x0E-22 %x24-FF) ; UTF8 without EOL or "#"
216             'Safe_String' => [ qr/\G([\x01-\x09\x0b-\x0c\x0e-\x22\x24-\xff]+)/ ],
217              
218             # Positive-Integer = ("1" / "2" / "3" / "4" / "5" / "6" / "7" / "8" / "9") *DIGIT
219             'Positive_Integer' => [ qr/\G([1-9][0-9]*)/, sub { 0 + $1 } ],
220              
221             # Number-Of-Tests = 1*DIGIT
222             'Number_Of_Tests' => [ qr/\G(\d+)/, sub { 0 + $1 } ],
223             );
224              
225 2     2   939 method lex ($input, $pos, $expected) {
  55     55   47  
  55         48  
  55         35  
226 55         39 my @matches;
227              
228 55         55 TOKEN: for my $token_name (@$expected) {
229 189         196 my $token = $tokens{$token_name};
230 189 50       224 die "Unknown token $token_name" unless defined $token;
231 189         143 my $rule = $token->[0];
232 189         220 pos($$input) = $pos;
233 189 100       630 next TOKEN unless $$input =~ $rule;
234              
235 62         134 my $matched_len = $+[0] - $-[0];
236 62         65 my $matched_value = undef;
237              
238 62 100       135 if (defined( my $val = $token->[1] )) {
    100          
239 32 100       47 if (ref $val eq 'CODE') {
240 15         27 $matched_value = $val->();
241             } else {
242 17         23 $matched_value = $val;
243             }
244             } elsif ($#- > 0) { # Captured a value
245 8         11 $matched_value = $1;
246             }
247              
248 62         107 push @matches, [ $token_name, \$matched_value, $matched_len ];
249              
250 62 100       119 if ($token_name eq 'Safe_String') {
251 8 50       40 if ($self->exhaustive_strings) {
    50          
252 0         0 for my $len (reverse 1 .. $matched_len - 1) {
253 0         0 my $value = substr($matched_value, 0, $len);
254 0         0 push @matches, [ $token_name, \$value, $len ];
255             }
256             } elsif ($matched_value =~ /(.*) $/) {
257 0         0 my $value = $1;
258 0         0 push @matches, [ $token_name, \$value, $matched_len - 1 ];
259             }
260             }
261             }
262              
263 55         87 return @matches;
264             }
265              
266 2     2   960 method parse_line ($line) {
  16     16   16  
  16         22  
  16         15  
267 16         30 my $rec = Marpa::R2::Recognizer->new({
268             grammar => $self->line_grammar,
269             ranking_method => 'rule',
270             # trace_terminals => 2,
271             # trace_values => 1,
272             # trace_actions => 1,
273             });
274              
275 16         1373 for my $pos (0 .. length($line) - 1) {
276 90         141 my $expected_tokens = $rec->terminals_expected;
277              
278 90 100       1580 if (@$expected_tokens) {
279 55         100 my @matching_tokens = $self->lex(\$line, $pos, $expected_tokens);
280 55         140 $rec->alternative( @$_ ) for @matching_tokens;
281             }
282              
283 90         1285 my $ok = eval {
284 90         135 $rec->earleme_complete;
285 89         1071 1;
286             };
287 90 100       194 if (!$ok) {
288 1         8 return [ 'Junk_Line', $line ];
289             }
290             }
291              
292 15         32 $rec->end_input;
293              
294 15         85 return ${$rec->value};
  15         45  
295             }
296              
297 2     2   733 method parse {
  5     5   182  
  5         5  
298 5         16 my $rec = Marpa::R2::Recognizer->new({
299             grammar => $self->stream_grammar,
300             ranking_method => 'rule',
301             # trace_terminals => 2,
302             # trace_values => 1,
303             # trace_actions => 1,
304             });
305              
306 5         615 my $reader = $self->reader;
307              
308 5         10 while (defined( my $line = $reader->() )) {
309             # print "Expecting: ", join(" ", @{ $rec->terminals_expected }), "\n";
310 16         31 my $line_token = $self->parse_line($line);
311 16 100       687 next if $line_token->[0] eq 'Junk_Line'; # XXX do something cooler
312 15 100       49 unless (defined $rec->read(@$line_token)) {
313 1         30 my $expected = $rec->terminals_expected;
314 1         62 die "Parse error, expecting [@$expected], got $line_token->[0]";
315             }
316             }
317              
318 4         12 $rec->read('EOF');
319              
320 4         150 return ${$rec->value};
  4         11  
321             }
322              
323 2     2   358 no Mouse;
  2         3  
  2         11  
324              
325             package TAP::Spec::Parser::Actions;
326             BEGIN {
327 2     2   1522 $TAP::Spec::Parser::Actions::AUTHORITY = 'cpan:ARODLAND';
328             }
329             {
330             $TAP::Spec::Parser::Actions::VERSION = '0.07_991'; # TRIAL
331             }
332              
333             sub subrule1 {
334 38     38   45921 $_[1];
335             }
336              
337             sub subrule2 {
338 14     14   31007 $_[2];
339             }
340              
341             sub tokenize_TAP_Line {
342 8     8   153 [ 'TAP_Line', $_[1] ];
343             }
344              
345             sub tokenize_Version {
346 1     1   57 [ 'Version', $_[1] ];
347             }
348              
349             sub tokenize_Plan {
350 6     6   113 [ 'Plan', $_[1] ];
351             }
352              
353             sub tokenize_Comment {
354 0     0   0 [ 'Comment', $_[1] ];
355             }
356              
357             sub Testset {
358 4     4   170 my %tmp;
359 4   33     26 $tmp{header} = $_[1] || TAP::Spec::Header->new;
360 4         9 $tmp{plan} = $_[2][0];
361 4         7 $tmp{body} = $_[2][1];
362 4   33     14 $tmp{footer} = $_[3] || TAP::Spec::Footer->new;
363              
364 4         50 TAP::Spec::TestSet->new(%tmp);
365             }
366              
367             sub Plan_Body {
368 4     4   137 my $plan = $_[1];
369 4         6 my $body = $_[2];
370 4         7 [ $plan, $body ];
371             }
372              
373             sub Body_Plan {
374 0     0   0 my $body = $_[1];
375 0         0 my $plan = $_[2];
376 0         0 [ $plan, $body ];
377             }
378              
379             sub Header {
380 4     4   5824 my %tmp;
381 4 50       12 $tmp{comments} = $_[1] if defined $_[1];
382 4 100       11 $tmp{version} = $_[2] if defined $_[2];
383 4         57 TAP::Spec::Header->new(%tmp);
384             }
385              
386             # Footer = [Comments]
387             sub Footer {
388 4     4   62 my %tmp;
389 4 50       10 $tmp{comments} = $_[1] if defined $_[1];
390 4         29 TAP::Spec::Footer->new(%tmp);
391             }
392              
393             # Body = *(Comment / TAP-Line)
394             sub Body {
395 4     4   74 shift;
396 4         6 my @lines = @_;
397 4         35 TAP::Spec::Body->new(lines => \@lines);
398             }
399              
400             sub Comments {
401 0     0   0 shift;
402 0         0 my @comments = @_;
403 0         0 return \@comments;
404             }
405              
406             sub Version {
407 1     1   25 my $version_number = $_[3];
408 1         12 TAP::Spec::Version->new(version_number => $version_number);
409             }
410              
411             sub Plan_Simple {
412 6     6   155 my $number_of_tests = $_[1];
413 6         69 TAP::Spec::Plan::Simple->new(number_of_tests => $number_of_tests);
414             }
415              
416             sub Plan_Todo {
417 0     0   0 my $number_of_tests = $_[1];
418 0         0 my $skipped_tests = $_[5];
419              
420 0         0 TAP::Spec::Plan::Todo->new(
421             number_of_tests => $number_of_tests,
422             skipped_tests => $skipped_tests,
423             );
424             }
425              
426             sub Test_Numbers {
427 0     0   0 shift;
428 0         0 my @test_numbers = @_;
429 0         0 \@test_numbers;
430             }
431              
432             sub Plan_Skip_All {
433 0     0   0 my $reason = $_[5];
434 0         0 TAP::Spec::Plan::SkipAll->new(
435             reason => $reason,
436             );
437             }
438              
439             sub Test_Result {
440 8     8   145 my %tmp;
441 8         16 $tmp{status} = $_[1];
442 8 50       20 $tmp{number} = $_[2] if defined $_[2];
443 8 50       15 $tmp{description} = $_[3] if defined $_[3];
444 8 50 33     16 $tmp{directive} = $_[4][0] if defined $_[4] && defined $_[4][0];
445 8 50 33     17 $tmp{reason} = $_[4][1] if defined $_[4] && defined $_[4][1];
446 8         83 TAP::Spec::TestResult->new(%tmp);
447             }
448              
449             sub Maybe_Directive_Reason {
450 0     0   0 my $directive = $_[4];
451 0         0 my $reason = $_[5];
452 0         0 return [ $directive, $reason ];
453             }
454              
455             sub Bail_Out {
456 0     0   0 my %tmp;
457 0 0       0 $tmp{reason} = $_[1] if defined $_[1];
458 0         0 TAP::Spec::BailOut->new( %tmp );
459             }
460              
461             sub Comment {
462 0     0   0 my $text = $_[1];
463 0         0 TAP::Spec::Comment->new( text => $text );
464             }
465              
466             sub String {
467 0     0   0 shift;
468 0         0 my @parts = @_;
469 0         0 return join "", @parts;
470             }
471              
472             sub undef {
473             undef
474 17     17   2197 }
475              
476             1;
477              
478             __END__