diff options
author | Paolo Bonzini <pbonzini@redhat.com> | 2020-11-18 12:31:46 +0100 |
---|---|---|
committer | Paolo Bonzini <pbonzini@redhat.com> | 2021-01-06 08:43:26 +0100 |
commit | ee5e7977e3e6a24806b467010deedc71898c0aa2 (patch) | |
tree | 75e163e289b61c59b33ba44188f969b6fd6d00a5 | |
parent | c9d9dacdbc9fad31f00f871b24ec6b99a611ff5e (diff) | |
download | meson-ee5e7977e3e6a24806b467010deedc71898c0aa2.zip meson-ee5e7977e3e6a24806b467010deedc71898c0aa2.tar.gz meson-ee5e7977e3e6a24806b467010deedc71898c0aa2.tar.bz2 |
mtest: allow parsing TAP line by line
This is the first step towards asynchronous parsing of the TAP output.
We will need to call the same code from both a "for" loop (for unit
tests) and an "async for" loop (for mtest itself). Because the same
function cannot be both a generator and an asynchronous generator, we
need to build both on a common core. This commit therefore introduces
a parse_line function that "parse" can call in a loop. All the local
variables of TAPParser.parse move into "self".
Signed-off-by: Paolo Bonzini <pbonzini@redhat.com>
-rw-r--r-- | mesonbuild/mtest.py | 131 |
1 files changed, 68 insertions, 63 deletions
diff --git a/mesonbuild/mtest.py b/mesonbuild/mtest.py index 9db271e..ea73a7c 100644 --- a/mesonbuild/mtest.py +++ b/mesonbuild/mtest.py @@ -206,6 +206,8 @@ class TestResult(enum.Enum): return decorator(result_str).get_text(colorize) +TYPE_TAPResult = T.Union['TAPParser.Test', 'TAPParser.Error', 'TAPParser.Version', 'TAPParser.Plan', 'TAPParser.Bailout'] + class TAPParser: Plan = namedtuple('Plan', ['count', 'late', 'skipped', 'explanation']) Bailout = namedtuple('Bailout', ['message']) @@ -225,6 +227,16 @@ class TAPParser: _RE_YAML_START = re.compile(r'(\s+)---.*') _RE_YAML_END = re.compile(r'\s+\.\.\.\s*') + found_late_test = False + bailed_out = False + plan: T.Optional[Plan] = None + lineno = 0 + num_tests = 0 + yaml_lineno: T.Optional[int] = None + yaml_indent = '' + state = _MAIN + version = 12 + def __init__(self, io: T.Iterator[str]): self.io = io @@ -246,64 +258,57 @@ class TAPParser: yield self.Test(num, name, TestResult.OK if ok else TestResult.FAIL, explanation) - def parse(self) -> T.Generator[T.Union['TAPParser.Test', 'TAPParser.Error', 'TAPParser.Version', 'TAPParser.Plan', 'TAPParser.Bailout'], None, None]: - found_late_test = False - bailed_out = False - plan = None - lineno = 0 - num_tests = 0 - yaml_lineno = None - yaml_indent = '' - state = self._MAIN - version = 12 - while True: - lineno += 1 - try: - line = next(self.io).rstrip() - except StopIteration: - break + def parse(self) -> T.Iterator[TYPE_TAPResult]: + for line in self.io: + yield from self.parse_line(line) + yield from self.parse_line(None) + + def parse_line(self, line: T.Optional[str]) -> T.Iterator[TYPE_TAPResult]: + if line is not None: + self.lineno += 1 + line = line.rstrip() # YAML blocks are only accepted after a test - if state == self._AFTER_TEST: - if version >= 13: + if self.state == self._AFTER_TEST: + if self.version >= 13: m = self._RE_YAML_START.match(line) if m: - state = self._YAML - yaml_lineno = lineno - yaml_indent = m.group(1) - continue - state = self._MAIN + self.state = self._YAML + self.yaml_lineno = self.lineno + self.yaml_indent = m.group(1) + return + self.state = self._MAIN - elif state == self._YAML: + elif self.state == self._YAML: if self._RE_YAML_END.match(line): - state = self._MAIN - continue - if line.startswith(yaml_indent): - continue - yield self.Error('YAML block not terminated (started on line {})'.format(yaml_lineno)) - state = self._MAIN + self.state = self._MAIN + return + if line.startswith(self.yaml_indent): + return + yield self.Error('YAML block not terminated (started on line {})'.format(self.yaml_lineno)) + self.state = self._MAIN - assert state == self._MAIN + assert self.state == self._MAIN if line.startswith('#'): - continue + return m = self._RE_TEST.match(line) if m: - if plan and plan.late and not found_late_test: + if self.plan and self.plan.late and not self.found_late_test: yield self.Error('unexpected test after late plan') - found_late_test = True - num_tests += 1 - num = num_tests if m.group(2) is None else int(m.group(2)) - if num != num_tests: + self.found_late_test = True + self.num_tests += 1 + num = self.num_tests if m.group(2) is None else int(m.group(2)) + if num != self.num_tests: yield self.Error('out of order test numbers') yield from self.parse_test(m.group(1) == 'ok', num, m.group(3), m.group(4), m.group(5)) - state = self._AFTER_TEST - continue + self.state = self._AFTER_TEST + return m = self._RE_PLAN.match(line) if m: - if plan: + if self.plan: yield self.Error('more than one plan found') else: count = int(m.group(1)) @@ -315,44 +320,44 @@ class TAPParser: skipped = True else: yield self.Error('invalid directive for plan') - plan = self.Plan(count=count, late=(num_tests > 0), - skipped=skipped, explanation=m.group(3)) - yield plan - continue + self.plan = self.Plan(count=count, late=(self.num_tests > 0), + skipped=skipped, explanation=m.group(3)) + yield self.plan + return m = self._RE_BAILOUT.match(line) if m: yield self.Bailout(m.group(1)) - bailed_out = True - continue + self.bailed_out = True + return m = self._RE_VERSION.match(line) if m: # The TAP version is only accepted as the first line - if lineno != 1: + if self.lineno != 1: yield self.Error('version number must be on the first line') - continue - version = int(m.group(1)) - if version < 13: + return + self.version = int(m.group(1)) + if self.version < 13: yield self.Error('version number should be at least 13') else: - yield self.Version(version=version) - continue + yield self.Version(version=self.version) + return if not line: - continue - - yield self.Error('unexpected input at line {}'.format((lineno,))) - - if state == self._YAML: - yield self.Error('YAML block not terminated (started on line {})'.format(yaml_lineno)) + return - if not bailed_out and plan and num_tests != plan.count: - if num_tests < plan.count: - yield self.Error('Too few tests run (expected {}, got {})'.format(plan.count, num_tests)) - else: - yield self.Error('Too many tests run (expected {}, got {})'.format(plan.count, num_tests)) + yield self.Error('unexpected input at line {}'.format((self.lineno,))) + else: + # end of file + if self.state == self._YAML: + yield self.Error('YAML block not terminated (started on line {})'.format(self.yaml_lineno)) + if not self.bailed_out and self.plan and self.num_tests != self.plan.count: + if self.num_tests < self.plan.count: + yield self.Error('Too few tests run (expected {}, got {})'.format(self.plan.count, self.num_tests)) + else: + yield self.Error('Too many tests run (expected {}, got {})'.format(self.plan.count, self.num_tests)) class TestLogger: def flush(self) -> None: |