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   2273 $TAP::Spec::Parser::AUTHORITY = 'cpan:ARODLAND';
4             }
5             {
6             $TAP::Spec::Parser::VERSION = '0.10';
7             }
8             # ABSTRACT: Reference implementation of the TAP specification
9 2     2   2384 use Mouse;
  2         181399  
  2         10  
10 2     2   3891 use Method::Signatures::Simple;
  2         98372  
  2         17  
11 2     2   1003 use Try::Tiny;
  2         4  
  2         124  
12 2     2   4410 use Marpa::R2 2.025_001;
  2         504892  
  2         104  
13 2     2   92792 use TAP::Spec::TestSet ();
  2         266  
  2         143  
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   1236 method new_from_string ($class: $string, %args) {
  5     5   11  
  5         54  
  5         9  
30 2 50   2   27 open my $fh, '<', \$string or die $!;
  2         3  
  2         19  
  5         225  
31             my $reader = sub {
32 20     20   1695 scalar <$fh>;
33 5         3158 };
34              
35 5         136 $class->new(%args, reader => $reader);
36             }
37              
38 2     2   1422 method parse_from_string ($class: $string, %args) {
  5     5   18917  
  5         18  
  5         14  
39 5         34 $class->new_from_string($string, %args)->parse;
40             }
41              
42              
43 2     2   1051 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   1611 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   925 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   1327 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   1070 method stream_grammar {
  5     5   12  
  5         13  
102 5         95 $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   910 method line_grammar {
  16     16   37  
  16         24  
196 16         245 $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   2628 method lex ($input, $pos, $expected) {
  55     55   78  
  55         160  
  55         63  
226 55         71 my @matches;
227              
228 55         101 TOKEN: for my $token_name (@$expected) {
229 189         332 my $token = $tokens{$token_name};
230 189 50       578 die "Unknown token $token_name" unless defined $token;
231 189         257 my $rule = $token->[0];
232 189         406 pos($$input) = $pos;
233 189 100       1124 next TOKEN unless $$input =~ $rule;
234              
235 62         214 my $matched_len = $+[0] - $-[0];
236 62         102 my $matched_value = undef;
237              
238 62 100       290 if (defined( my $val = $token->[1] )) {
    100          
239 32 100       71 if (ref $val eq 'CODE') {
240 15         44 $matched_value = $val->();
241             } else {
242 17         35 $matched_value = $val;
243             }
244             } elsif ($#- > 0) { # Captured a value
245 8         24 $matched_value = $1;
246             }
247              
248 62         164 push @matches, [ $token_name, \$matched_value, $matched_len ];
249              
250 62 100       199 if ($token_name eq 'Safe_String') {
251 8 50       83 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         162 return @matches;
264             }
265              
266 2     2   4519 method parse_line ($line) {
  16     16   35  
  16         33  
  16         32  
267 16         81 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         4093 for my $pos (0 .. length($line) - 1) {
276 90         277 my $expected_tokens = $rec->terminals_expected;
277              
278 90 100       3126 if (@$expected_tokens) {
279 55         173 my @matching_tokens = $self->lex(\$line, $pos, $expected_tokens);
280 55         258 $rec->alternative( @$_ ) for @matching_tokens;
281             }
282              
283 90         2101 my $ok = eval {
284 90         255 $rec->earleme_complete;
285 89         2203 1;
286             };
287 90 100       400 if (!$ok) {
288 1         13 return [ 'Junk_Line', $line ];
289             }
290             }
291              
292 15         70 $rec->end_input;
293              
294 15         187 return ${$rec->value};
  15         78  
295             }
296              
297 2     2   1114 method parse {
  5     5   222  
  5         10  
298 5         26 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         1124 my $reader = $self->reader;
307              
308 5         18 while (defined( my $line = $reader->() )) {
309             # print "Expecting: ", join(" ", @{ $rec->terminals_expected }), "\n";
310 16         71 my $line_token = $self->parse_line($line);
311 16 100       1264 next if $line_token->[0] eq 'Junk_Line'; # XXX do something cooler
312 15 100       207 unless (defined $rec->read(@$line_token)) {
313 1         94 my $expected = $rec->terminals_expected;
314 1         88 die "Parse error, expecting [@$expected], got $line_token->[0]";
315             }
316             }
317              
318 4         19 $rec->read('EOF');
319              
320 4         265 return ${$rec->value};
  4         20  
321             }
322              
323 2     2   528 no Mouse;
  2         6  
  2         17  
324              
325             package TAP::Spec::Parser::Actions;
326             BEGIN {
327 2     2   5133 $TAP::Spec::Parser::Actions::AUTHORITY = 'cpan:ARODLAND';
328             }
329             {
330             $TAP::Spec::Parser::Actions::VERSION = '0.10';
331             }
332              
333             sub subrule1 {
334 38     38   89938 $_[1];
335             }
336              
337             sub subrule2 {
338 14     14   66525 $_[2];
339             }
340              
341             sub tokenize_TAP_Line {
342 8     8   308 [ 'TAP_Line', $_[1] ];
343             }
344              
345             sub tokenize_Version {
346 1     1   122 [ 'Version', $_[1] ];
347             }
348              
349             sub tokenize_Plan {
350 6     6   199 [ 'Plan', $_[1] ];
351             }
352              
353             sub tokenize_Comment {
354 0     0   0 [ 'Comment', $_[1] ];
355             }
356              
357             sub Testset {
358 4     4   232 my %tmp;
359 4   33     34 $tmp{header} = $_[1] || TAP::Spec::Header->new;
360 4         15 $tmp{plan} = $_[2][0];
361 4         14 $tmp{body} = $_[2][1];
362 4   33     23 $tmp{footer} = $_[3] || TAP::Spec::Footer->new;
363              
364 4         85 TAP::Spec::TestSet->new(%tmp);
365             }
366              
367             sub Plan_Body {
368 4     4   227 my $plan = $_[1];
369 4         8 my $body = $_[2];
370 4         15 [ $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   9550 my %tmp;
381 4 50       240 $tmp{comments} = $_[1] if defined $_[1];
382 4 100       22 $tmp{version} = $_[2] if defined $_[2];
383 4         104 TAP::Spec::Header->new(%tmp);
384             }
385              
386             # Footer = [Comments]
387             sub Footer {
388 4     4   128 my %tmp;
389 4 50       14 $tmp{comments} = $_[1] if defined $_[1];
390 4         53 TAP::Spec::Footer->new(%tmp);
391             }
392              
393             # Body = *(Comment / TAP-Line)
394             sub Body {
395 4     4   135 shift;
396 4         13 my @lines = @_;
397 4         58 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   44 my $version_number = $_[3];
408 1         26 TAP::Spec::Version->new(version_number => $version_number);
409             }
410              
411             sub Plan_Simple {
412 6     6   957 my $number_of_tests = $_[1];
413 6         148 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   323 my %tmp;
441 8         29 $tmp{status} = $_[1];
442 8 50       43 $tmp{number} = $_[2] if defined $_[2];
443 8 50       31 $tmp{description} = $_[3] if defined $_[3];
444 8 50 33     28 $tmp{directive} = $_[4][0] if defined $_[4] && defined $_[4][0];
445 8 50 33     29 $tmp{reason} = $_[4][1] if defined $_[4] && defined $_[4][1];
446 8         197 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   3903 }
475              
476             1;
477              
478             __END__