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
)
29 plan
= re
.compile(r
"""
30 ^1..(?P<expected>\d+) # Match the plan details.
31 [^#]* # Consume any non-hash character to confirm only
32 # directives appear with the plan details.
33 \#? # Optional directive marker.
34 \s* # Optional whitespace.
35 (?P<directive>.*) # Optional directive text.
37 diagnostic
= re
.compile(r
'^#')
38 bail
= re
.compile(r
"""
40 \s* # Optional whitespace.
41 (?P<reason>.*) # Optional reason.
43 version
= re
.compile(r
'^TAP version (?P<version>\d+)$')
45 TAP_MINIMUM_DECLARED_VERSION
= 13
47 def parse_file(self
, filename
):
48 """Parse a TAP file to an iterable of tap.line.Line objects.
50 This is a generator method that will yield an object for each
51 parsed line. The file given by `filename` is assumed to exist.
53 return self
.parse(open(filename
, 'r'))
55 def parse_stdin(self
):
56 """Parse a TAP stream from standard input.
58 Note: this has the side effect of closing the standard input
59 filehandle after parsing.
61 return self
.parse(sys
.stdin
)
63 def parse_text(self
, text
):
64 """Parse a string containing one or more lines of TAP output."""
65 return self
.parse(StringIO(text
))
68 """Generate tap.line.Line objects, given a file-like object `fh`.
70 `fh` may be any object that implements both the iterator and
71 context management protocol (i.e. it can be used in both a
72 "with" statement and a "for...in" statement.)
74 Trailing whitespace and newline characters will be automatically
75 stripped from the input lines.
79 yield self
.parse_line(line
.rstrip())
81 def parse_line(self
, text
):
82 """Parse a line into whatever TAP category it belongs."""
83 match
= self
.ok
.match(text
)
85 return self
._parse
_result
(True, match
)
87 match
= self
.not_ok
.match(text
)
89 return self
._parse
_result
(False, match
)
91 if self
.diagnostic
.match(text
):
92 return Diagnostic(text
)
94 match
= self
.plan
.match(text
)
96 return self
._parse
_plan
(match
)
98 match
= self
.bail
.match(text
)
100 return Bail(match
.group('reason'))
102 match
= self
.version
.match(text
)
104 return self
._parse
_version
(match
)
108 def _parse_plan(self
, match
):
109 """Parse a matching plan line."""
110 expected_tests
= int(match
.group('expected'))
111 directive
= Directive(match
.group('directive'))
113 # Only SKIP directives are allowed in the plan.
114 if directive
.text
and not directive
.skip
:
117 return Plan(expected_tests
, directive
)
119 def _parse_result(self
, ok
, match
):
120 """Parse a matching result line into a result instance."""
122 ok
, match
.group('number'), match
.group('description').strip(),
123 Directive(match
.group('directive')))
125 def _parse_version(self
, match
):
126 version
= int(match
.group('version'))
127 if version
< self
.TAP_MINIMUM_DECLARED_VERSION
:
128 raise ValueError(_('It is an error to explicitly specify '
129 'any version lower than 13.'))
130 return Version(version
)