diff options
author | Paolo Bonzini <pbonzini@redhat.com> | 2021-01-14 09:43:46 +0100 |
---|---|---|
committer | Paolo Bonzini <pbonzini@redhat.com> | 2021-01-15 10:03:54 +0100 |
commit | 1f52e2fa45396046d1694fcbccf15ba2f8381ce2 (patch) | |
tree | 5968148ae1f2fe00c3fcd6d879d82386200cefb6 | |
parent | 9e42048e3caf86b9981d3a9a43ec6eeb1fe316f5 (diff) | |
download | meson-1f52e2fa45396046d1694fcbccf15ba2f8381ce2.zip meson-1f52e2fa45396046d1694fcbccf15ba2f8381ce2.tar.gz meson-1f52e2fa45396046d1694fcbccf15ba2f8381ce2.tar.bz2 |
mtest: turn TestRun into a hierarchy
Make SimpleTestRunner oblivious of the various test protocols. All
the different "complete_*" and "parse_*" methods move to the
subclasses of TestRun.
-rw-r--r-- | mesonbuild/mtest.py | 219 |
1 files changed, 128 insertions, 91 deletions
diff --git a/mesonbuild/mtest.py b/mesonbuild/mtest.py index ecdffd6..63fa5b4 100644 --- a/mesonbuild/mtest.py +++ b/mesonbuild/mtest.py @@ -731,6 +731,10 @@ class JunitBuilder(TestLogger): class TestRun: TEST_NUM = 0 + PROTOCOL_TO_CLASS: T.Dict[TestProtocol, T.Type['TestRun']] = {} + + def __new__(cls, test: TestSerialisation, *args: T.Any, **kwargs: T.Any) -> T.Any: + return super().__new__(TestRun.PROTOCOL_TO_CLASS[test.protocol]) def __init__(self, test: TestSerialisation, test_env: T.Dict[str, str], name: str, timeout: T.Optional[int]): @@ -756,80 +760,6 @@ class TestRun: self.starttime = time.time() self.cmd = cmd - def complete_gtest(self, returncode: int, - stdo: T.Optional[str], stde: T.Optional[str]) -> None: - filename = '{}.xml'.format(self.test.name) - if self.test.workdir: - filename = os.path.join(self.test.workdir, filename) - self.junit = et.parse(filename) - self.complete_exitcode(returncode, stdo, stde) - - def complete_exitcode(self, returncode: int, - stdo: T.Optional[str], stde: T.Optional[str]) -> None: - if returncode == GNU_SKIP_RETURNCODE: - res = TestResult.SKIP - elif returncode == GNU_ERROR_RETURNCODE: - res = TestResult.ERROR - else: - res = TestResult.FAIL if bool(returncode) else TestResult.OK - self.complete(returncode, res, stdo, stde) - - async def parse_tap(self, lines: T.AsyncIterator[str]) -> T.Tuple[TestResult, str]: - res = TestResult.OK - error = '' - - async for i in TAPParser().parse_async(lines): - if isinstance(i, TAPParser.Bailout): - res = TestResult.ERROR - elif isinstance(i, TAPParser.Test): - self.results.append(i) - if i.result.is_bad(): - res = TestResult.FAIL - elif isinstance(i, TAPParser.Error): - error = '\nTAP parsing error: ' + i.message - res = TestResult.ERROR - - if all(t.result is TestResult.SKIP for t in self.results): - # This includes the case where self.results is empty - res = TestResult.SKIP - return res, error - - def complete_tap(self, returncode: int, res: TestResult, - stdo: str, stde: str) -> None: - if returncode != 0 and not res.was_killed(): - res = TestResult.ERROR - stde += '\n(test program exited with status code {})'.format(returncode,) - - self.complete(returncode, res, stdo, stde) - - async def parse_rust(self, lines: T.AsyncIterator[str]) -> T.Tuple[TestResult, str]: - def parse_res(n: int, name: str, result: str) -> TAPParser.Test: - if result == 'ok': - return TAPParser.Test(n, name, TestResult.OK, None) - elif result == 'ignored': - return TAPParser.Test(n, name, TestResult.SKIP, None) - elif result == 'FAILED': - return TAPParser.Test(n, name, TestResult.FAIL, None) - return TAPParser.Test(n, name, TestResult.ERROR, - 'Unsupported output from rust test: {}'.format(result)) - - n = 1 - async for line in lines: - if line.startswith('test ') and not line.startswith('test result'): - _, name, _, result = line.rstrip().split(' ') - name = name.replace('::', '.') - self.results.append(parse_res(n, name, result)) - n += 1 - - if all(t.result is TestResult.SKIP for t in self.results): - # This includes the case where self.results is empty - return TestResult.SKIP, '' - elif any(t.result is TestResult.ERROR for t in self.results): - return TestResult.ERROR, '' - elif any(t.result is TestResult.FAIL for t in self.results): - return TestResult.FAIL, '' - return TestResult.OK, '' - @property def num(self) -> int: if self._num is None: @@ -853,8 +783,8 @@ class TestRun: return '{}/{} subtests passed'.format(passed, ran) return '' - def complete(self, returncode: int, res: TestResult, - stdo: T.Optional[str], stde: T.Optional[str]) -> None: + def _complete(self, returncode: int, res: TestResult, + stdo: T.Optional[str], stde: T.Optional[str]) -> None: assert isinstance(res, TestResult) if self.should_fail and res in (TestResult.OK, TestResult.FAIL): res = TestResult.UNEXPECTEDPASS if res.is_ok() else TestResult.EXPECTEDFAIL @@ -867,7 +797,11 @@ class TestRun: def complete_skip(self, message: str) -> None: self.starttime = time.time() - self.complete(GNU_SKIP_RETURNCODE, TestResult.SKIP, message, None) + self._complete(GNU_SKIP_RETURNCODE, TestResult.SKIP, message, None) + + def complete(self, returncode: int, res: TestResult, + stdo: T.Optional[str], stde: T.Optional[str]) -> None: + self._complete(returncode, res, stdo, stde) def get_log(self) -> str: res = '--- command ---\n' @@ -904,6 +838,118 @@ class TestRun: log += '\n'.join(lines[-100:]) return log + @property + def needs_parsing(self) -> bool: + return False + + async def parse(self, lines: T.AsyncIterator[str]) -> T.Tuple[TestResult, str]: + async for l in lines: + pass + return TestResult.OK, '' + + +class TestRunExitCode(TestRun): + + def complete(self, returncode: int, res: TestResult, + stdo: T.Optional[str], stde: T.Optional[str]) -> None: + if res: + pass + elif returncode == GNU_SKIP_RETURNCODE: + res = TestResult.SKIP + elif returncode == GNU_ERROR_RETURNCODE: + res = TestResult.ERROR + else: + res = TestResult.FAIL if bool(returncode) else TestResult.OK + super().complete(returncode, res, stdo, stde) + +TestRun.PROTOCOL_TO_CLASS[TestProtocol.EXITCODE] = TestRunExitCode + + +class TestRunGTest(TestRunExitCode): + def complete(self, returncode: int, res: TestResult, + stdo: T.Optional[str], stde: T.Optional[str]) -> None: + filename = '{}.xml'.format(self.test.name) + if self.test.workdir: + filename = os.path.join(self.test.workdir, filename) + + self.junit = et.parse(filename) + super().complete(returncode, res, stdo, stde) + +TestRun.PROTOCOL_TO_CLASS[TestProtocol.GTEST] = TestRunGTest + + +class TestRunTAP(TestRun): + @property + def needs_parsing(self) -> bool: + return True + + def complete(self, returncode: int, res: TestResult, + stdo: str, stde: str) -> None: + if returncode != 0 and not res.was_killed(): + res = TestResult.ERROR + stde += '\n(test program exited with status code {})'.format(returncode,) + + super().complete(returncode, res, stdo, stde) + + async def parse(self, lines: T.AsyncIterator[str]) -> T.Tuple[TestResult, str]: + res = TestResult.OK + error = '' + + async for i in TAPParser().parse_async(lines): + if isinstance(i, TAPParser.Bailout): + res = TestResult.ERROR + elif isinstance(i, TAPParser.Test): + self.results.append(i) + if i.result.is_bad(): + res = TestResult.FAIL + elif isinstance(i, TAPParser.Error): + error = '\nTAP parsing error: ' + i.message + res = TestResult.ERROR + + if all(t.result is TestResult.SKIP for t in self.results): + # This includes the case where self.results is empty + res = TestResult.SKIP + return res, error + +TestRun.PROTOCOL_TO_CLASS[TestProtocol.TAP] = TestRunTAP + + +class TestRunRust(TestRun): + @property + def needs_parsing(self) -> bool: + return True + + async def parse(self, lines: T.AsyncIterator[str]) -> T.Tuple[TestResult, str]: + def parse_res(n: int, name: str, result: str) -> TAPParser.Test: + if result == 'ok': + return TAPParser.Test(n, name, TestResult.OK, None) + elif result == 'ignored': + return TAPParser.Test(n, name, TestResult.SKIP, None) + elif result == 'FAILED': + return TAPParser.Test(n, name, TestResult.FAIL, None) + return TAPParser.Test(n, name, TestResult.ERROR, + 'Unsupported output from rust test: {}'.format(result)) + + n = 1 + async for line in lines: + if line.startswith('test ') and not line.startswith('test result'): + _, name, _, result = line.rstrip().split(' ') + name = name.replace('::', '.') + self.results.append(parse_res(n, name, result)) + n += 1 + + if all(t.result is TestResult.SKIP for t in self.results): + # This includes the case where self.results is empty + return TestResult.SKIP, '' + elif any(t.result is TestResult.ERROR for t in self.results): + return TestResult.ERROR, '' + elif any(t.result is TestResult.FAIL for t in self.results): + return TestResult.FAIL, '' + return TestResult.OK, '' + +TestRun.PROTOCOL_TO_CLASS[TestProtocol.RUST] = TestRunRust + + def decode(stream: T.Union[None, bytes]) -> str: if stream is None: return '' @@ -1154,7 +1200,7 @@ class SingleTestRunner: self.runobj.start(cmd) stdout = None stderr = None - if self.test.protocol is TestProtocol.TAP: + if self.runobj.needs_parsing: stdout = asyncio.subprocess.PIPE stderr = None if self.options.verbose else asyncio.subprocess.PIPE elif not self.options.verbose: @@ -1192,10 +1238,9 @@ class SingleTestRunner: nonlocal stdo stdo = ''.join(stdo_lines) - if self.test.protocol is TestProtocol.TAP: - parse_task = self.runobj.parse_tap(lines()) - elif self.test.protocol is TestProtocol.RUST: - parse_task = self.runobj.parse_rust(lines()) + parse_task = None + if self.runobj.needs_parsing: + parse_task = self.runobj.parse(lines()) elif stdout is not None: stdo_task = p.stdout.read(-1) if stderr is not None and stderr != asyncio.subprocess.STDOUT: @@ -1216,16 +1261,8 @@ class SingleTestRunner: if error: stde += '\n' + error result = result or res - if self.test.protocol is TestProtocol.TAP: - self.runobj.complete_tap(returncode, result, stdo, stde) - return - if result: - self.runobj.complete(returncode, result, stdo, stde) - elif self.test.protocol is TestProtocol.EXITCODE: - self.runobj.complete_exitcode(returncode, stdo, stde) - elif self.test.protocol is TestProtocol.GTEST: - self.runobj.complete_gtest(returncode, stdo, stde) + self.runobj.complete(returncode, result, stdo, stde) class TestHarness: |