1 # Copyright (c) 2016, Matt Layman
3 from io
import StringIO
7 from tap
.directive
import Directive
9 from tap
.line
import Bail
, Diagnostic
, Plan
, Result
, Unknown
, Version
13 """A parser for TAP files and lines."""
15 # ok and not ok share most of the same characteristics.
17 \s* # Optional whitespace.
18 (?P<number>\d*) # Optional test number.
19 \s* # Optional whitespace.
20 (?P<description>[^#]*) # Optional description before #.
21 \#? # Optional directive marker.
22 \s* # Optional whitespace.
23 (?P<directive>.*) # Optional directive text.
25 ok
= re
.compile(r
'^ok' + result_base
, re
.VERBOSE
)
26 not_ok
= re
.compile(r
'^not\ ok' + result_base
, re
.VERBOSE
)
27 plan
= re
.compile(r
"""
28 ^1..(?P<expected>\d+) # Match the plan details.
29 [^#]* # Consume any non-hash character to confirm only
30 # directives appear with the plan details.
31 \#? # Optional directive marker.
32 \s* # Optional whitespace.
33 (?P<directive>.*) # Optional directive text.
35 diagnostic
= re
.compile(r
'^#')
36 bail
= re
.compile(r
"""
38 \s* # Optional whitespace.
39 (?P<reason>.*) # Optional reason.
41 version
= re
.compile(r
'^TAP version (?P<version>\d+)$')
43 TAP_MINIMUM_DECLARED_VERSION
= 13
45 def parse_file(self
, filename
):
46 """Parse a TAP file to an iterable of tap.line.Line objects.
48 This is a generator method that will yield an object for each
49 parsed line. The file given by `filename` is assumed to exist.
51 return self
.parse(open(filename
, 'r'))
53 def parse_stdin(self
):
54 """Parse a TAP stream from standard input.
56 Note: this has the side effect of closing the standard input
57 filehandle after parsing.
59 return self
.parse(sys
.stdin
)
61 def parse_text(self
, text
):
62 """Parse a string containing one or more lines of TAP output."""
63 return self
.parse(StringIO(text
))
66 """Generate tap.line.Line objects, given a file-like object `fh`.
68 `fh` may be any object that implements both the iterator and
69 context management protocol (i.e. it can be used in both a
70 "with" statement and a "for...in" statement.)
72 Trailing whitespace and newline characters will be automatically
73 stripped from the input lines.
77 yield self
.parse_line(line
.rstrip())
79 def parse_line(self
, text
):
80 """Parse a line into whatever TAP category it belongs."""
81 match
= self
.ok
.match(text
)
83 return self
._parse
_result
(True, match
)
85 match
= self
.not_ok
.match(text
)
87 return self
._parse
_result
(False, match
)
89 if self
.diagnostic
.match(text
):
90 return Diagnostic(text
)
92 match
= self
.plan
.match(text
)
94 return self
._parse
_plan
(match
)
96 match
= self
.bail
.match(text
)
98 return Bail(match
.group('reason'))
100 match
= self
.version
.match(text
)
102 return self
._parse
_version
(match
)
106 def _parse_plan(self
, match
):
107 """Parse a matching plan line."""
108 expected_tests
= int(match
.group('expected'))
109 directive
= Directive(match
.group('directive'))
111 # Only SKIP directives are allowed in the plan.
112 if directive
.text
and not directive
.skip
:
115 return Plan(expected_tests
, directive
)
117 def _parse_result(self
, ok
, match
):
118 """Parse a matching result line into a result instance."""
120 ok
, match
.group('number'), match
.group('description').strip(),
121 Directive(match
.group('directive')))
123 def _parse_version(self
, match
):
124 version
= int(match
.group('version'))
125 if version
< self
.TAP_MINIMUM_DECLARED_VERSION
:
126 raise ValueError(_('It is an error to explicitly specify '
127 'any version lower than 13.'))
128 return Version(version
)