Commit | Line | Data |
---|---|---|
0235b0db MJ |
1 | # SPDX-License-Identifier: BSD-2-Clause |
2 | # | |
b85894a3 MJ |
3 | # Copyright (c) 2016, Matt Layman |
4 | ||
5 | from io import StringIO | |
6 | import re | |
7 | import sys | |
8 | ||
9 | from tap.directive import Directive | |
10 | from tap.i18n import _ | |
11 | from tap.line import Bail, Diagnostic, Plan, Result, Unknown, Version | |
12 | ||
13 | ||
14 | class Parser(object): | |
15 | """A parser for TAP files and lines.""" | |
16 | ||
17 | # ok and not ok share most of the same characteristics. | |
18 | result_base = r""" | |
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. | |
26 | """ | |
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. | |
36 | """, re.VERBOSE) | |
37 | diagnostic = re.compile(r'^#') | |
38 | bail = re.compile(r""" | |
39 | ^Bail\ out! | |
40 | \s* # Optional whitespace. | |
41 | (?P<reason>.*) # Optional reason. | |
42 | """, re.VERBOSE) | |
43 | version = re.compile(r'^TAP version (?P<version>\d+)$') | |
44 | ||
45 | TAP_MINIMUM_DECLARED_VERSION = 13 | |
46 | ||
47 | def parse_file(self, filename): | |
48 | """Parse a TAP file to an iterable of tap.line.Line objects. | |
49 | ||
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. | |
52 | """ | |
53 | return self.parse(open(filename, 'r')) | |
54 | ||
55 | def parse_stdin(self): | |
56 | """Parse a TAP stream from standard input. | |
57 | ||
58 | Note: this has the side effect of closing the standard input | |
59 | filehandle after parsing. | |
60 | """ | |
61 | return self.parse(sys.stdin) | |
62 | ||
63 | def parse_text(self, text): | |
64 | """Parse a string containing one or more lines of TAP output.""" | |
65 | return self.parse(StringIO(text)) | |
66 | ||
67 | def parse(self, fh): | |
68 | """Generate tap.line.Line objects, given a file-like object `fh`. | |
69 | ||
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.) | |
73 | ||
74 | Trailing whitespace and newline characters will be automatically | |
75 | stripped from the input lines. | |
76 | """ | |
77 | with fh: | |
78 | for line in fh: | |
79 | yield self.parse_line(line.rstrip()) | |
80 | ||
81 | def parse_line(self, text): | |
82 | """Parse a line into whatever TAP category it belongs.""" | |
83 | match = self.ok.match(text) | |
84 | if match: | |
85 | return self._parse_result(True, match) | |
86 | ||
87 | match = self.not_ok.match(text) | |
88 | if match: | |
89 | return self._parse_result(False, match) | |
90 | ||
91 | if self.diagnostic.match(text): | |
92 | return Diagnostic(text) | |
93 | ||
94 | match = self.plan.match(text) | |
95 | if match: | |
96 | return self._parse_plan(match) | |
97 | ||
98 | match = self.bail.match(text) | |
99 | if match: | |
100 | return Bail(match.group('reason')) | |
101 | ||
102 | match = self.version.match(text) | |
103 | if match: | |
104 | return self._parse_version(match) | |
105 | ||
106 | return Unknown() | |
107 | ||
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')) | |
112 | ||
113 | # Only SKIP directives are allowed in the plan. | |
114 | if directive.text and not directive.skip: | |
115 | return Unknown() | |
116 | ||
117 | return Plan(expected_tests, directive) | |
118 | ||
119 | def _parse_result(self, ok, match): | |
120 | """Parse a matching result line into a result instance.""" | |
121 | return Result( | |
122 | ok, match.group('number'), match.group('description').strip(), | |
123 | Directive(match.group('directive'))) | |
124 | ||
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) |