1 # SPDX-License-Identifier: BSD-2-Clause
3 # Copyright (c) 2016, Matt Layman
5 from io
import StringIO
9 from tap
.directive
import Directive
10 from tap
.i18n
import _
11 from tap
.line
import Bail
, Diagnostic
, Plan
, Result
, Unknown
, Version
15 """A parser for TAP files and lines."""
17 # ok and not ok share most of the same characteristics.
19 \s* # Optional whitespace.
20 (?P<number>\d*) # Optional test number.
21 \s* # Optional whitespace.
22 (?P<description>[^#]*) # Optional description before #.
23 \#? # Optional directive marker.
24 \s* # Optional whitespace.
25 (?P<directive>.*) # Optional directive text.
27 ok
= re
.compile(r
'^ok' + result_base
, re
.VERBOSE
)
28 not_ok
= re
.compile(r
'^not\ ok' + result_base
, re
.VERBOSE
)
31 ^1..(?P<expected>\d+) # Match the plan details.
32 [^#]* # Consume any non-hash character to confirm only
33 # directives appear with the plan details.
34 \#? # Optional directive marker.
35 \s* # Optional whitespace.
36 (?P<directive>.*) # Optional directive text.
40 diagnostic
= re
.compile(r
'^#')
44 \s* # Optional whitespace.
45 (?P<reason>.*) # Optional reason.
49 version
= re
.compile(r
'^TAP version (?P<version>\d+)$')
51 TAP_MINIMUM_DECLARED_VERSION
= 13
53 def parse_file(self
, filename
):
54 """Parse a TAP file to an iterable of tap.line.Line objects.
56 This is a generator method that will yield an object for each
57 parsed line. The file given by `filename` is assumed to exist.
59 return self
.parse(open(filename
, 'r'))
61 def parse_stdin(self
):
62 """Parse a TAP stream from standard input.
64 Note: this has the side effect of closing the standard input
65 filehandle after parsing.
67 return self
.parse(sys
.stdin
)
69 def parse_text(self
, text
):
70 """Parse a string containing one or more lines of TAP output."""
71 return self
.parse(StringIO(text
))
74 """Generate tap.line.Line objects, given a file-like object `fh`.
76 `fh` may be any object that implements both the iterator and
77 context management protocol (i.e. it can be used in both a
78 "with" statement and a "for...in" statement.)
80 Trailing whitespace and newline characters will be automatically
81 stripped from the input lines.
85 yield self
.parse_line(line
.rstrip())
87 def parse_line(self
, text
):
88 """Parse a line into whatever TAP category it belongs."""
89 match
= self
.ok
.match(text
)
91 return self
._parse
_result
(True, match
)
93 match
= self
.not_ok
.match(text
)
95 return self
._parse
_result
(False, match
)
97 if self
.diagnostic
.match(text
):
98 return Diagnostic(text
)
100 match
= self
.plan
.match(text
)
102 return self
._parse
_plan
(match
)
104 match
= self
.bail
.match(text
)
106 return Bail(match
.group('reason'))
108 match
= self
.version
.match(text
)
110 return self
._parse
_version
(match
)
114 def _parse_plan(self
, match
):
115 """Parse a matching plan line."""
116 expected_tests
= int(match
.group('expected'))
117 directive
= Directive(match
.group('directive'))
119 # Only SKIP directives are allowed in the plan.
120 if directive
.text
and not directive
.skip
:
123 return Plan(expected_tests
, directive
)
125 def _parse_result(self
, ok
, match
):
126 """Parse a matching result line into a result instance."""
129 match
.group('number'),
130 match
.group('description').strip(),
131 Directive(match
.group('directive')),
134 def _parse_version(self
, match
):
135 version
= int(match
.group('version'))
136 if version
< self
.TAP_MINIMUM_DECLARED_VERSION
:
138 _('It is an error to explicitly specify ' 'any version lower than 13.')
140 return Version(version
)