summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTom Mewett <tom.mewett@codethink.co.uk>2019-08-21 16:37:49 +0100
committerbst-marge-bot <marge-bot@buildstream.build>2019-08-30 09:28:18 +0000
commit0ca7169a1fe4dfedb3524db35387e900bd4f4b10 (patch)
tree6fb920a34a19f56e116d1a46b2e251ef46b9cef6
parentaa9bc230681486fea5f7d23e35bcfd62ad673310 (diff)
downloadbuildstream-0ca7169a1fe4dfedb3524db35387e900bd4f4b10.tar.gz
tests: Implement and register in_subprocess pytest mark
-rw-r--r--setup.cfg2
-rw-r--r--src/buildstream/testing/_forked.py94
-rwxr-xr-xtests/conftest.py22
3 files changed, 118 insertions, 0 deletions
diff --git a/setup.cfg b/setup.cfg
index 06dcd8fd4..7bade2c48 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -20,6 +20,8 @@ env =
D:XDG_CACHE_HOME=./tmp/cache
D:XDG_CONFIG_HOME=./tmp/config
D:XDG_DATA_HOME=./tmp/share
+markers =
+ in_subprocess: run test in a Python process forked from the main one
[pycodestyle]
max-line-length = 119
diff --git a/src/buildstream/testing/_forked.py b/src/buildstream/testing/_forked.py
new file mode 100644
index 000000000..af5e9c070
--- /dev/null
+++ b/src/buildstream/testing/_forked.py
@@ -0,0 +1,94 @@
+# This code was based on pytest-forked, commit 6098c1, found here:
+# <https://github.com/pytest-dev/pytest-forked>
+# Its copyright notice is included below.
+#
+# 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.
+
+import os
+import marshal
+
+import py
+import pytest
+# XXX Using pytest private internals here
+from _pytest import runner
+
+EXITSTATUS_TESTEXIT = 4
+
+
+# copied from xdist remote
+def serialize_report(rep):
+ d = rep.__dict__.copy()
+ if hasattr(rep.longrepr, 'toterminal'):
+ d['longrepr'] = str(rep.longrepr)
+ else:
+ d['longrepr'] = rep.longrepr
+ for name in d:
+ if isinstance(d[name], py.path.local): # pylint: disable=no-member
+ d[name] = str(d[name])
+ elif name == "result":
+ d[name] = None # for now
+ return d
+
+
+def forked_run_report(item):
+ def runforked():
+ try:
+ reports = runner.runtestprotocol(item, log=False)
+ except KeyboardInterrupt:
+ os._exit(EXITSTATUS_TESTEXIT)
+ return marshal.dumps([serialize_report(x) for x in reports])
+
+ ff = py.process.ForkedFunc(runforked) # pylint: disable=no-member
+ result = ff.waitfinish()
+ if result.retval is not None:
+ report_dumps = marshal.loads(result.retval)
+ return [runner.TestReport(**x) for x in report_dumps]
+ else:
+ if result.exitstatus == EXITSTATUS_TESTEXIT:
+ pytest.exit("forked test item %s raised Exit" % (item,))
+ return [report_process_crash(item, result)]
+
+
+def report_process_crash(item, result):
+ try:
+ from _pytest.compat import getfslineno
+ except ImportError:
+ # pytest<4.2
+ path, lineno = item._getfslineno()
+ else:
+ path, lineno = getfslineno(item)
+ info = ("%s:%s: running the test CRASHED with signal %d" %
+ (path, lineno, result.signal))
+
+ # We need to create a CallInfo instance that is pre-initialised to contain
+ # info about an exception. We do this by using a function which does
+ # 0/0. Also, the API varies between pytest versions.
+ has_from_call = getattr(runner.CallInfo, "from_call", None) is not None
+ if has_from_call: # pytest >= 4.1
+ call = runner.CallInfo.from_call(lambda: 0 / 0, "???")
+ else:
+ call = runner.CallInfo(lambda: 0 / 0, "???")
+ call.excinfo = info
+
+ rep = runner.pytest_runtest_makereport(item, call)
+ if result.out:
+ rep.sections.append(("captured stdout", result.out))
+ if result.err:
+ rep.sections.append(("captured stderr", result.err))
+ return rep
diff --git a/tests/conftest.py b/tests/conftest.py
index d6b0b02e0..7728fb5c8 100755
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -23,6 +23,7 @@ import os
import pytest
from buildstream.testing import register_repo_kind, sourcetests_collection_hook
+from buildstream.testing._forked import forked_run_report
from buildstream.testing.integration import integration_cache # pylint: disable=unused-import
@@ -68,6 +69,27 @@ def pytest_runtest_setup(item):
#################################################
+# in_subprocess mark #
+#################################################
+#
+# Various issues can occur when forking the Python process and using gRPC,
+# due to its multithreading. As BuildStream forks for parallelisation, gRPC
+# features are restricted to child processes, so tests using them must also
+# run as child processes. The in_subprocess mark handles this.
+# See <https://github.com/grpc/grpc/blob/master/doc/fork_support.md>.
+#
+@pytest.mark.tryfirst
+def pytest_runtest_protocol(item):
+ if item.get_closest_marker('in_subprocess') is not None:
+ reports = forked_run_report(item)
+ for rep in reports:
+ item.ihook.pytest_runtest_logreport(report=rep)
+ return True
+ else:
+ return None
+
+
+#################################################
# remote_services fixture #
#################################################
#