mirror of
https://github.com/scylladb/scylladb.git
synced 2026-04-26 03:20:37 +00:00
Add the possibility to run boost test from pytest. Boost facade based on code from https://github.com/pytest-dev/pytest-cpp, but enhanced and rewritten to suite better.
187 lines
7.1 KiB
Python
187 lines
7.1 KiB
Python
#
|
|
# Copyright (c) 2014 Bruno Oliveira
|
|
#
|
|
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
# of this software and associated documentation files (the "Software"), to deal
|
|
# in the Software without restriction, including without limitation the rights
|
|
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
# copies of the Software, and to permit persons to whom the Software is
|
|
# furnished to do so, subject to the following conditions:
|
|
#
|
|
# The above copyright notice and this permission notice shall be included in all
|
|
# copies or substantial portions of the Software.
|
|
#
|
|
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
# SOFTWARE.
|
|
#
|
|
# Copyright (C) 2025-present ScyllaDB
|
|
#
|
|
# SPDX-License-Identifier: LicenseRef-ScyllaDB-Source-Available-1.0
|
|
#
|
|
from __future__ import annotations
|
|
|
|
import io
|
|
import os
|
|
import subprocess
|
|
from collections.abc import Sequence
|
|
from pathlib import Path
|
|
from xml.etree import ElementTree
|
|
|
|
from test.pylib.cpp.facade import CppTestFacade, CppTestFailure, run_process
|
|
|
|
TIMEOUT_DEBUG = 60 * 5 # seconds
|
|
TIMEOUT = 60 * 2 # seconds
|
|
COMBINED_TESTS = Path('build', 'dev', 'test', 'boost', 'combined_tests')
|
|
|
|
class BoostTestFacade(CppTestFacade):
|
|
"""
|
|
Facade for BoostTests that's responsible for discovering test functions and executing them correctly.
|
|
"""
|
|
|
|
def list_tests(
|
|
self,
|
|
executable: Path,
|
|
no_parallel: bool,
|
|
) -> tuple[bool, list[str]]:
|
|
"""
|
|
Return a boolean value indicating whether the tests combined or not and the list of tests
|
|
"""
|
|
if no_parallel:
|
|
return False, [os.path.basename(os.path.splitext(executable)[0])]
|
|
else:
|
|
if not os.path.isfile(executable):
|
|
return True, self.combined_suites[executable.stem]
|
|
args = [executable, '--list_content']
|
|
try:
|
|
output = subprocess.check_output(
|
|
args,
|
|
stderr=subprocess.STDOUT,
|
|
universal_newlines=True,
|
|
)
|
|
except subprocess.CalledProcessError as e:
|
|
output = e.output
|
|
# --list_content produces the list of all test cases in the file. When BOOST_DATA_TEST_CASE is used it
|
|
# additionally produce the lines with numbers for each case preserving the function name like this:
|
|
# test_singular_tree_ptr_sz*
|
|
# _0*
|
|
# _1*
|
|
# _2*
|
|
# however, it's only possible to run test_singular_tree_ptr_sz that executes all test cases
|
|
# this line catches only test function name ignoring unrelated lines like '_0'
|
|
# Note: this ignores any test case starting with a '_' symbol
|
|
return False, [case[:-1] for case in output.splitlines() if
|
|
case.endswith('*') and not case.strip().startswith('_')]
|
|
|
|
def run_test(
|
|
self,
|
|
executable: Path,
|
|
original_name: str,
|
|
test_name: str,
|
|
mode: str,
|
|
file_name: Path,
|
|
test_args:Sequence[str] = (),
|
|
) -> tuple[list[CppTestFailure], str] | tuple[None, str]:
|
|
def read_file(name: Path) -> str:
|
|
try:
|
|
with io.open(name) as f:
|
|
return f.read()
|
|
except IOError:
|
|
return ''
|
|
|
|
timeout = TIMEOUT_DEBUG if mode=='debug' else TIMEOUT
|
|
root_log_dir = self.temp_dir / mode / 'pytest'
|
|
log_xml = root_log_dir / f"{test_name}.log"
|
|
stdout_file_path = root_log_dir/ f"{test_name}_stdout.log"
|
|
stderr_file_path = root_log_dir / f"{test_name}_stderr.log"
|
|
report_xml = root_log_dir / f"{test_name}.xml"
|
|
args = [ str(executable),
|
|
'--output_format=XML',
|
|
f"--report_sink={report_xml}",
|
|
f"--log_sink={log_xml}",
|
|
'--catch_system_errors=no',
|
|
'--color_output=false',
|
|
]
|
|
if original_name != Path(executable).stem:
|
|
if executable.stem == COMBINED_TESTS.stem:
|
|
args.append(f"--run_test={file_name.stem}/{original_name}")
|
|
else:
|
|
args.append(f"--run_test={original_name}")
|
|
# Tests are written in the way that everything after '--' passes to the test itself rather than to the test framework
|
|
args.append('--')
|
|
args.extend(test_args)
|
|
os.chdir(self.temp_dir.parent)
|
|
p, stderr, stdout = run_process(args, timeout)
|
|
|
|
with open(stdout_file_path, 'w') as fd:
|
|
fd.write(stdout)
|
|
with open(stderr_file_path, 'w') as fd:
|
|
fd.write(stderr)
|
|
log = read_file(log_xml)
|
|
report = read_file(report_xml)
|
|
|
|
results = self._parse_log(log=log)
|
|
|
|
if p.returncode != 0:
|
|
msg = (
|
|
'working_dir: {working_dir}\n'
|
|
'Internal Error: calling {executable} '
|
|
'for test {test_id} failed (return_code={return_code}):\n'
|
|
'output file:{stdout}\n'
|
|
'std error file:{stderr}\n'
|
|
'log:{log}\n'
|
|
'report:{report}\n'
|
|
'command to repeat:{command}'
|
|
)
|
|
failure = CppTestFailure(
|
|
file_name.name,
|
|
line_num=results[0].line_num,
|
|
contents=msg.format(
|
|
working_dir=os.getcwd(),
|
|
executable=executable,
|
|
test_id=test_name,
|
|
stdout=stdout_file_path.absolute(),
|
|
stderr=stderr_file_path.absolute(),
|
|
log=log,
|
|
report=report,
|
|
command=' '.join(p.args),
|
|
return_code=p.returncode,
|
|
),
|
|
)
|
|
return [failure], stdout
|
|
|
|
if results:
|
|
return results, stdout
|
|
|
|
return None, stdout
|
|
|
|
def _parse_log(self, log: str) -> list[CppTestFailure]:
|
|
"""
|
|
Parse the 'log' section produced by BoostTest.
|
|
|
|
This is always an XML file, and from this it's possible to parse most of the
|
|
failures possible when running BoostTest.
|
|
"""
|
|
parsed_elements = []
|
|
|
|
log_root = ElementTree.fromstring(log)
|
|
|
|
if log_root is not None:
|
|
parsed_elements.extend(log_root.findall('Exception'))
|
|
parsed_elements.extend(log_root.findall('Error'))
|
|
parsed_elements.extend(log_root.findall('FatalError'))
|
|
|
|
result = []
|
|
for elem in parsed_elements:
|
|
last_checkpoint = elem.find('LastCheckpoint')
|
|
if last_checkpoint:
|
|
elem = last_checkpoint
|
|
file_name = elem.attrib['file']
|
|
line_num = int(elem.attrib['line'])
|
|
result.append(CppTestFailure(file_name, line_num, elem.text or ''))
|
|
return result
|