~thopiekar/pacman-pm/pacman

« back to all changes in this revision

Viewing changes to build-aux/tap-driver.py

  • Committer: Allan McRae
  • Author(s): Dave Reisner
  • Date: 2019-08-12 00:03:17 UTC
  • Revision ID: git-v1:18a64400617259b34ccf014682fd8022d551a036
meson: remove tap-driver.py, use meson's TAP protocol

This includes a patch from Andrew to fix pactest's TAP output for
subtests. Original TAP support in meson was added in 0.50, but 0.51
contains a bugfix that ensures the test still work with the --verbose
flag passed to meson test, so let's depend on that.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
 
#!/usr/bin/env python3
2
 
# Adapted from tappy copyright (c) 2016, Matt Layman
3
 
# MIT license
4
 
# https://github.com/python-tap/tappy
5
 
 
6
 
import io
7
 
import re
8
 
import subprocess
9
 
import sys
10
 
 
11
 
 
12
 
class Directive(object):
13
 
    """A representation of a result line directive."""
14
 
 
15
 
    skip_pattern = re.compile(
16
 
        r"""^SKIP\S*
17
 
            (?P<whitespace>\s*) # Optional whitespace.
18
 
            (?P<reason>.*)      # Slurp up the rest.""",
19
 
        re.IGNORECASE | re.VERBOSE)
20
 
    todo_pattern = re.compile(
21
 
        r"""^TODO\b             # The directive name
22
 
            (?P<whitespace>\s*) # Immediately following must be whitespace.
23
 
            (?P<reason>.*)      # Slurp up the rest.""",
24
 
        re.IGNORECASE | re.VERBOSE)
25
 
 
26
 
    def __init__(self, text):
27
 
        """Initialize the directive by parsing the text.
28
 
        The text is assumed to be everything after a '#\s*' on a result line.
29
 
        """
30
 
        self._text = text
31
 
        self._skip = False
32
 
        self._todo = False
33
 
        self._reason = None
34
 
 
35
 
        match = self.skip_pattern.match(text)
36
 
        if match:
37
 
            self._skip = True
38
 
            self._reason = match.group('reason')
39
 
 
40
 
        match = self.todo_pattern.match(text)
41
 
        if match:
42
 
            if match.group('whitespace'):
43
 
                self._todo = True
44
 
            else:
45
 
                # Catch the case where the directive has no descriptive text.
46
 
                if match.group('reason') == '':
47
 
                    self._todo = True
48
 
            self._reason = match.group('reason')
49
 
 
50
 
    @property
51
 
    def text(self):
52
 
        """Get the entire text."""
53
 
        return self._text
54
 
 
55
 
    @property
56
 
    def skip(self):
57
 
        """Check if the directive is a SKIP type."""
58
 
        return self._skip
59
 
 
60
 
    @property
61
 
    def todo(self):
62
 
        """Check if the directive is a TODO type."""
63
 
        return self._todo
64
 
 
65
 
    @property
66
 
    def reason(self):
67
 
        """Get the reason for the directive."""
68
 
        return self._reason
69
 
 
70
 
 
71
 
class Parser(object):
72
 
    """A parser for TAP files and lines."""
73
 
 
74
 
    # ok and not ok share most of the same characteristics.
75
 
    result_base = r"""
76
 
        \s*                    # Optional whitespace.
77
 
        (?P<number>\d*)        # Optional test number.
78
 
        \s*                    # Optional whitespace.
79
 
        (?P<description>[^#]*) # Optional description before #.
80
 
        \#?                    # Optional directive marker.
81
 
        \s*                    # Optional whitespace.
82
 
        (?P<directive>.*)      # Optional directive text.
83
 
    """
84
 
    ok = re.compile(r'^ok' + result_base, re.VERBOSE)
85
 
    not_ok = re.compile(r'^not\ ok' + result_base, re.VERBOSE)
86
 
    plan = re.compile(r"""
87
 
        ^1..(?P<expected>\d+) # Match the plan details.
88
 
        [^#]*                 # Consume any non-hash character to confirm only
89
 
                              # directives appear with the plan details.
90
 
        \#?                   # Optional directive marker.
91
 
        \s*                   # Optional whitespace.
92
 
        (?P<directive>.*)     # Optional directive text.
93
 
    """, re.VERBOSE)
94
 
    diagnostic = re.compile(r'^#')
95
 
    bail = re.compile(r"""
96
 
        ^Bail\ out!
97
 
        \s*            # Optional whitespace.
98
 
        (?P<reason>.*) # Optional reason.
99
 
    """, re.VERBOSE)
100
 
    version = re.compile(r'^TAP version (?P<version>\d+)$')
101
 
 
102
 
    TAP_MINIMUM_DECLARED_VERSION = 13
103
 
 
104
 
    def parse(self, fh):
105
 
        """Generate tap.line.Line objects, given a file-like object `fh`.
106
 
        `fh` may be any object that implements both the iterator and
107
 
        context management protocol (i.e. it can be used in both a
108
 
        "with" statement and a "for...in" statement.)
109
 
        Trailing whitespace and newline characters will be automatically
110
 
        stripped from the input lines.
111
 
        """
112
 
        with fh:
113
 
            for line in fh:
114
 
                yield self.parse_line(line.rstrip())
115
 
 
116
 
    def parse_line(self, text):
117
 
        """Parse a line into whatever TAP category it belongs."""
118
 
        match = self.ok.match(text)
119
 
        if match:
120
 
            return self._parse_result(True, match)
121
 
 
122
 
        match = self.not_ok.match(text)
123
 
        if match:
124
 
            return self._parse_result(False, match)
125
 
 
126
 
        if self.diagnostic.match(text):
127
 
            return ('diagnostic', text)
128
 
 
129
 
        match = self.plan.match(text)
130
 
        if match:
131
 
            return self._parse_plan(match)
132
 
 
133
 
        match = self.bail.match(text)
134
 
        if match:
135
 
            return ('bail', match.group('reason'))
136
 
 
137
 
        match = self.version.match(text)
138
 
        if match:
139
 
            return self._parse_version(match)
140
 
 
141
 
        return ('unknown',)
142
 
 
143
 
    def _parse_plan(self, match):
144
 
        """Parse a matching plan line."""
145
 
        expected_tests = int(match.group('expected'))
146
 
        directive = Directive(match.group('directive'))
147
 
 
148
 
        # Only SKIP directives are allowed in the plan.
149
 
        if directive.text and not directive.skip:
150
 
            return ('unknown',)
151
 
 
152
 
        return ('plan', expected_tests, directive)
153
 
 
154
 
    def _parse_result(self, ok, match):
155
 
        """Parse a matching result line into a result instance."""
156
 
        return ('result', ok, match.group('number'),
157
 
            match.group('description').strip(),
158
 
            Directive(match.group('directive')))
159
 
 
160
 
    def _parse_version(self, match):
161
 
        version = int(match.group('version'))
162
 
        if version < self.TAP_MINIMUM_DECLARED_VERSION:
163
 
            raise ValueError('It is an error to explicitly specify '
164
 
                             'any version lower than 13.')
165
 
        return ('version', version)
166
 
 
167
 
 
168
 
class Rules(object):
169
 
 
170
 
    def __init__(self):
171
 
        self._lines_seen = {'plan': [], 'test': 0, 'failed': 0, 'version': []}
172
 
        self._errors = []
173
 
 
174
 
    def check(self, final_line_count):
175
 
        """Check the status of all provided data and update the suite."""
176
 
        if self._lines_seen['version']:
177
 
            self._process_version_lines()
178
 
        self._process_plan_lines(final_line_count)
179
 
 
180
 
    def check_errors(self):
181
 
        if self._lines_seen['failed'] > 0:
182
 
            self._add_error('Tests failed.')
183
 
        if self._errors:
184
 
            for error in self._errors:
185
 
                print(error)
186
 
            return 1
187
 
        return 0
188
 
 
189
 
    def _process_version_lines(self):
190
 
        """Process version line rules."""
191
 
        if len(self._lines_seen['version']) > 1:
192
 
            self._add_error('Multiple version lines appeared.')
193
 
        elif self._lines_seen['version'][0] != 1:
194
 
            self._add_error('The version must be on the first line.')
195
 
 
196
 
    def _process_plan_lines(self, final_line_count):
197
 
        """Process plan line rules."""
198
 
        if not self._lines_seen['plan']:
199
 
            self._add_error('Missing a plan.')
200
 
            return
201
 
 
202
 
        if len(self._lines_seen['plan']) > 1:
203
 
            self._add_error('Only one plan line is permitted per file.')
204
 
            return
205
 
 
206
 
        expected_tests, at_line = self._lines_seen['plan'][0]
207
 
        if not self._plan_on_valid_line(at_line, final_line_count):
208
 
            self._add_error(
209
 
                'A plan must appear at the beginning or end of the file.')
210
 
            return
211
 
 
212
 
        if expected_tests != self._lines_seen['test']:
213
 
            self._add_error(
214
 
                'Expected {expected_count} tests '
215
 
                'but only {seen_count} ran.'.format(
216
 
                    expected_count=expected_tests,
217
 
                    seen_count=self._lines_seen['test']))
218
 
 
219
 
    def _plan_on_valid_line(self, at_line, final_line_count):
220
 
        """Check if a plan is on a valid line."""
221
 
        # Put the common cases first.
222
 
        if at_line == 1 or at_line == final_line_count:
223
 
            return True
224
 
 
225
 
        # The plan may only appear on line 2 if the version is at line 1.
226
 
        after_version = (
227
 
            self._lines_seen['version'] and
228
 
            self._lines_seen['version'][0] == 1 and
229
 
            at_line == 2)
230
 
        if after_version:
231
 
            return True
232
 
 
233
 
        return False
234
 
 
235
 
    def handle_bail(self, reason):
236
 
        """Handle a bail line."""
237
 
        self._add_error('Bailed: {reason}').format(reason=reason)
238
 
 
239
 
    def handle_skipping_plan(self):
240
 
        """Handle a plan that contains a SKIP directive."""
241
 
        sys.exit(77)
242
 
 
243
 
    def saw_plan(self, expected_tests, at_line):
244
 
        """Record when a plan line was seen."""
245
 
        self._lines_seen['plan'].append((expected_tests, at_line))
246
 
 
247
 
    def saw_test(self, ok):
248
 
        """Record when a test line was seen."""
249
 
        self._lines_seen['test'] += 1
250
 
        if not ok:
251
 
            self._lines_seen['failed'] += 1
252
 
 
253
 
    def saw_version_at(self, line_counter):
254
 
        """Record when a version line was seen."""
255
 
        self._lines_seen['version'].append(line_counter)
256
 
 
257
 
    def _add_error(self, message):
258
 
        self._errors += [message]
259
 
 
260
 
 
261
 
if __name__ == '__main__':
262
 
    parser = Parser()
263
 
    rules = Rules()
264
 
 
265
 
    try:
266
 
        out = subprocess.check_output(sys.argv[1:], universal_newlines=True)
267
 
    except subprocess.CalledProcessError as e:
268
 
        sys.stdout.write(e.output)
269
 
        raise e
270
 
 
271
 
    line_generator = parser.parse(io.StringIO(out))
272
 
    line_counter = 0
273
 
    for line in line_generator:
274
 
        line_counter += 1
275
 
 
276
 
        if line[0] == 'unknown':
277
 
            continue
278
 
 
279
 
        if line[0] == 'result':
280
 
            rules.saw_test(line[1])
281
 
            print('{okay} {num} {description} {directive}'.format(
282
 
                okay=('' if line[1] else 'not ') + 'ok', num=line[2],
283
 
                description=line[3], directive=line[4].text))
284
 
        elif line[0] == 'plan':
285
 
            if line[2].skip:
286
 
                rules.handle_skipping_plan()
287
 
            rules.saw_plan(line[1], line_counter)
288
 
        elif line[0] == 'bail':
289
 
            rules.handle_bail(line[1])
290
 
        elif line[0] == 'version':
291
 
            rules.saw_version_at(line_counter)
292
 
        elif line[0] == 'diagnostic':
293
 
            print(line[1])
294
 
 
295
 
    rules.check(line_counter)
296
 
    sys.exit(rules.check_errors())