Add native support for gtest tests

Gtest can output junit results with a command line switch. We can parse
this to get more detailed results than the returncode, and put those in
our own Junit output. We basically just throw away the top level
'testsuites' object, then fixup the names of the tests, and shove that
into our junit.
pull/7064/head
Dylan Baker 5 years ago
parent 0c51762463
commit 083c5f6357
  1. 13
      docs/markdown/Reference-manual.md
  2. 6
      docs/markdown/snippets/gtest_protocol.md
  3. 5
      mesonbuild/backend/backends.py
  4. 6
      mesonbuild/interpreter.py
  5. 54
      mesonbuild/mtest.py
  6. 10
      run_unittests.py
  7. 4
      test cases/frameworks/2 gtest/meson.build

@ -1662,11 +1662,14 @@ test(..., env: nomalloc, ...)
before test is executed even if they have `build_by_default : false`.
Since 0.46.0
- `protocol` specifies how the test results are parsed and can be one
of `exitcode` (the executable's exit code is used by the test harness
to record the outcome of the test) or `tap` ([Test Anything
Protocol](https://www.testanything.org/)). For more on the Meson test
harness protocol read [Unit Tests](Unit-tests.md). Since 0.50.0
- `protocol` *(Since 0.50.0)* specifies how the test results are parsed and can
be one of `exitcode`, `tap`, or `gtest`. For more information about test
harness protocol read [Unit Tests](Unit-tests.md). The following values are
accepted:
- `exitcode`: the executable's exit code is used by the test harness
to record the outcome of the test)
- `tap` ([Test Anything Protocol](https://www.testanything.org/))
- `gtest`. *(Since 0.55.0)* for Google Tests.
- `priority` specifies the priority of a test. Tests with a
higher priority are *started* before tests with a lower priority.

@ -0,0 +1,6 @@
## Test protocol for gtest
Due to the popularity of Gtest (google test) among C and C++ developers meson
now supports a special protocol for gtest. With this protocol meson injects
arguments to gtests to output JUnit, reads that JUnit, and adds the output to
the JUnit it generates.

@ -42,6 +42,7 @@ class TestProtocol(enum.Enum):
EXITCODE = 0
TAP = 1
GTEST = 2
@classmethod
def from_str(cls, string: str) -> 'TestProtocol':
@ -49,11 +50,15 @@ class TestProtocol(enum.Enum):
return cls.EXITCODE
elif string == 'tap':
return cls.TAP
elif string == 'gtest':
return cls.GTEST
raise MesonException('unknown test format {}'.format(string))
def __str__(self) -> str:
if self is self.EXITCODE:
return 'exitcode'
elif self is self.GTEST:
return 'gtest'
return 'tap'

@ -3772,6 +3772,8 @@ This will become a hard error in the future.''' % kwargs['input'], location=self
@FeatureNewKwargs('test', '0.52.0', ['priority'])
@permittedKwargs(permitted_kwargs['test'])
def func_test(self, node, args, kwargs):
if kwargs.get('protocol') == 'gtest':
FeatureNew('"gtest" protocol for tests', '0.55.0').use(self.subproject)
self.add_test(node, args, kwargs, True)
def unpack_env_kwarg(self, kwargs) -> build.EnvironmentVariables:
@ -3823,8 +3825,8 @@ This will become a hard error in the future.''' % kwargs['input'], location=self
if not isinstance(timeout, int):
raise InterpreterException('Timeout must be an integer.')
protocol = kwargs.get('protocol', 'exitcode')
if protocol not in ('exitcode', 'tap'):
raise InterpreterException('Protocol must be "exitcode" or "tap".')
if protocol not in {'exitcode', 'tap', 'gtest'}:
raise InterpreterException('Protocol must be "exitcode", "tap", or "gtest".')
suite = []
prj = self.subproject if self.is_subproject() else self.build.project_name
for s in mesonlib.stringlistify(kwargs.get('suite', '')):

@ -94,7 +94,10 @@ def add_arguments(parser: argparse.ArgumentParser) -> None:
help='List available tests.')
parser.add_argument('--wrapper', default=None, dest='wrapper', type=split_args,
help='wrapper to run tests with (e.g. Valgrind)')
parser.add_argument('-C', default='.', dest='wd', type=os.path.abspath,
parser.add_argument('-C', default='.', dest='wd',
# https://github.com/python/typeshed/issues/3107
# https://github.com/python/mypy/issues/7177
type=os.path.abspath, # type: ignore
help='directory to cd into before running')
parser.add_argument('--suite', default=[], dest='include_suites', action='append', metavar='SUITE',
help='Only run tests belonging to the given suite.')
@ -349,6 +352,19 @@ class JunitBuilder:
def log(self, name: str, test: 'TestRun') -> None:
"""Log a single test case."""
if test.junit is not None:
for suite in test.junit.findall('.//testsuite'):
# Assume that we don't need to merge anything here...
suite.attrib['name'] = '{}.{}.{}'.format(test.project, name, suite.attrib['name'])
# GTest can inject invalid attributes
for case in suite.findall('.//testcase[@result]'):
del case.attrib['result']
for case in suite.findall('.//testcase[@timestamp]'):
del case.attrib['timestamp']
self.root.append(suite)
return
# In this case we have a test binary with multiple results.
# We want to record this so that each result is recorded
# separately
@ -429,11 +445,25 @@ class JunitBuilder:
class TestRun:
@classmethod
def make_gtest(cls, test: 'TestSerialisation', test_env: T.Dict[str, str],
returncode: int, starttime: float, duration: float,
stdo: T.Optional[str], stde: T.Optional[str],
cmd: T.Optional[T.List[str]]) -> 'TestRun':
filename = '{}.xml'.format(test.name)
if test.workdir:
filename = os.path.join(test.workdir, filename)
tree = et.parse(filename)
return cls.make_exitcode(
test, test_env, returncode, starttime, duration, stdo, stde, cmd,
junit=tree)
@classmethod
def make_exitcode(cls, test: 'TestSerialisation', test_env: T.Dict[str, str],
returncode: int, starttime: float, duration: float,
stdo: T.Optional[str], stde: T.Optional[str],
cmd: T.Optional[T.List[str]]) -> 'TestRun':
cmd: T.Optional[T.List[str]], **kwargs) -> 'TestRun':
if returncode == GNU_SKIP_RETURNCODE:
res = TestResult.SKIP
elif returncode == GNU_ERROR_RETURNCODE:
@ -442,15 +472,15 @@ class TestRun:
res = TestResult.EXPECTEDFAIL if bool(returncode) else TestResult.UNEXPECTEDPASS
else:
res = TestResult.FAIL if bool(returncode) else TestResult.OK
return cls(test, test_env, res, [], returncode, starttime, duration, stdo, stde, cmd)
return cls(test, test_env, res, [], returncode, starttime, duration, stdo, stde, cmd, **kwargs)
@classmethod
def make_tap(cls, test: 'TestSerialisation', test_env: T.Dict[str, str],
returncode: int, starttime: float, duration: float,
stdo: str, stde: str,
cmd: T.Optional[T.List[str]]) -> 'TestRun':
res = None # T.Optional[TestResult]
results = [] # T.List[TestResult]
res = None # type: T.Optional[TestResult]
results = [] # type: T.List[TestResult]
failed = False
for i in TAPParser(io.StringIO(stdo)).parse():
@ -486,7 +516,7 @@ class TestRun:
res: TestResult, results: T.List[TestResult], returncode:
int, starttime: float, duration: float,
stdo: T.Optional[str], stde: T.Optional[str],
cmd: T.Optional[T.List[str]]):
cmd: T.Optional[T.List[str]], *, junit: T.Optional[et.ElementTree] = None):
assert isinstance(res, TestResult)
self.res = res
self.results = results # May be an empty list
@ -499,6 +529,7 @@ class TestRun:
self.env = test_env
self.should_fail = test.should_fail
self.project = test.project_name
self.junit = junit
def get_log(self) -> str:
res = '--- command ---\n'
@ -652,7 +683,14 @@ class SingleTestRunner:
# errors avoid not being able to use the terminal.
os.setsid() # type: ignore
p = subprocess.Popen(cmd,
extra_cmd = [] # type: T.List[str]
if self.test.protocol is TestProtocol.GTEST:
gtestname = '{}.xml'.format(self.test.name)
if self.test.workdir:
gtestname = '{}:{}'.format(self.test.workdir, self.test.name)
extra_cmd.append('--gtest_output=xml:{}'.format(gtestname))
p = subprocess.Popen(cmd + extra_cmd,
stdout=stdout,
stderr=stderr,
env=self.env,
@ -744,6 +782,8 @@ class SingleTestRunner:
else:
if self.test.protocol is TestProtocol.EXITCODE:
return TestRun.make_exitcode(self.test, self.test_env, p.returncode, starttime, duration, stdo, stde, cmd)
elif self.test.protocol is TestProtocol.GTEST:
return TestRun.make_gtest(self.test, self.test_env, p.returncode, starttime, duration, stdo, stde, cmd)
else:
if self.options.verbose:
print(stdo, end='')

@ -4625,8 +4625,7 @@ recommended as it is not supported on some platforms''')
schema = et.XMLSchema(et.parse(str(Path(__file__).parent / 'data' / 'schema.xsd')))
testdir = os.path.join(self.common_test_dir, case)
self.init(testdir)
self.init(case)
self.run_tests()
junit = et.parse(str(Path(self.builddir) / 'meson-logs' / 'testlog.junit.xml'))
@ -4636,10 +4635,13 @@ recommended as it is not supported on some platforms''')
self.fail(e.error_log)
def test_junit_valid_tap(self):
self._test_junit('213 tap tests')
self._test_junit(os.path.join(self.common_test_dir, '213 tap tests'))
def test_junit_valid_exitcode(self):
self._test_junit('44 test args')
self._test_junit(os.path.join(self.common_test_dir, '44 test args'))
def test_junit_valid_gtest(self):
self._test_junit(os.path.join(self.framework_test_dir, '2 gtest'))
class FailureTests(BasePlatformTests):

@ -8,7 +8,7 @@ endif
gtest_nomain = dependency('gtest', main : false, method : 'system')
e = executable('testprog', 'test.cc', dependencies : gtest)
test('gtest test', e)
test('gtest test', e, protocol : 'gtest')
e = executable('testprog_nomain', 'test_nomain.cc', dependencies : gtest_nomain)
test('gtest nomain test', e)
test('gtest nomain test', e, protocol : 'gtest')

Loading…
Cancel
Save