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