summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorChristoph Reiter <reiter.christoph@gmail.com>2018-02-16 08:49:38 +0100
committerChristoph Reiter <reiter.christoph@gmail.com>2018-02-16 09:17:26 +0100
commite00e38f9c44568f7fab643a069f86c576011ddcc (patch)
tree5c25f85a9f1a775fd3cc8bfff7c1d76744178852
parentaae383cf44ee3eabcc4b4122049ea277524d5001 (diff)
downloadpygobject-e00e38f9c44568f7fab643a069f86c576011ddcc.tar.gz
tests: add a pytest hook for handling unhandled exception in closures
In PyGObject when an exception is raised in a closure called from C then the error gets passed to sys.excepthook (on the main thread at least) and the error is by default printed to stdout. Since pytest by default hides stdout, errors can be easily missed now. To make these errors more visible add a test wrapper which checks sys.excepthook for unhandled exceptions and reraises them. This makes the tests fail and as a bonus also shows the right stack trace instead of just the error message.
-rw-r--r--tests/Makefile.am1
-rw-r--r--tests/compathelper.py5
-rw-r--r--tests/conftest.py31
-rw-r--r--tests/test_glib.py13
-rw-r--r--tests/test_mainloop.py28
-rw-r--r--tests/test_option.py18
6 files changed, 65 insertions, 31 deletions
diff --git a/tests/Makefile.am b/tests/Makefile.am
index 216aca5c..f04610ab 100644
--- a/tests/Makefile.am
+++ b/tests/Makefile.am
@@ -148,6 +148,7 @@ EXTRA_DIST = \
test_resulttuple.py \
test_unknown.py \
test_ossig.py \
+ conftest.py \
__init__.py \
gi/__init__.py \
gi/overrides/__init__.py \
diff --git a/tests/compathelper.py b/tests/compathelper.py
index d4f4d27c..4ed38944 100644
--- a/tests/compathelper.py
+++ b/tests/compathelper.py
@@ -33,9 +33,14 @@ if sys.version_info >= (3, 0):
from io import StringIO
StringIO
PY3 = True
+
+ def reraise(tp, value, tb):
+ raise tp(value).with_traceback(tb)
else:
_long = long
_basestring = basestring
from StringIO import StringIO
StringIO
PY2 = True
+
+ exec("def reraise(tp, value, tb):\n raise tp, value, tb")
diff --git a/tests/conftest.py b/tests/conftest.py
new file mode 100644
index 00000000..f69405d4
--- /dev/null
+++ b/tests/conftest.py
@@ -0,0 +1,31 @@
+# -*- coding: utf-8 -*-
+
+import sys
+
+import pytest
+
+from .compathelper import reraise
+
+
+@pytest.hookimpl(hookwrapper=True)
+def pytest_runtest_call(item):
+ """A pytest hook which takes over sys.excepthook and raises any uncaught
+ exception (with PyGObject this happesn often when we get called from C,
+ like any signal handler, vfuncs tc)
+ """
+
+ assert sys.excepthook is sys.__excepthook__
+
+ exceptions = []
+
+ def on_hook(type_, value, tback):
+ exceptions.append((type_, value, tback))
+
+ sys.excepthook = on_hook
+ try:
+ yield
+ finally:
+ assert sys.excepthook in (on_hook, sys.__excepthook__)
+ sys.excepthook = sys.__excepthook__
+ if exceptions:
+ reraise(*exceptions[0])
diff --git a/tests/test_glib.py b/tests/test_glib.py
index 7a782e9c..8f481947 100644
--- a/tests/test_glib.py
+++ b/tests/test_glib.py
@@ -10,12 +10,25 @@ import os.path
import warnings
import subprocess
+import pytest
from gi.repository import GLib
from gi import PyGIDeprecationWarning
class TestGLib(unittest.TestCase):
+ @pytest.mark.xfail(strict=True)
+ def test_pytest_capture_error_in_closure(self):
+ # this test is supposed to fail
+ ml = GLib.MainLoop()
+
+ def callback():
+ ml.quit()
+ raise Exception("expected")
+
+ GLib.idle_add(callback)
+ ml.run()
+
@unittest.skipIf(os.name == "nt", "no bash on Windows")
def test_find_program_in_path(self):
bash_path = GLib.find_program_in_path('bash')
diff --git a/tests/test_mainloop.py b/tests/test_mainloop.py
index 1c1b1227..2d9fbd57 100644
--- a/tests/test_mainloop.py
+++ b/tests/test_mainloop.py
@@ -3,13 +3,14 @@
from __future__ import absolute_import
import os
-import sys
import select
import signal
import unittest
from gi.repository import GLib
+from .helper import capture_exceptions
+
class TestMainLoop(unittest.TestCase):
@@ -35,25 +36,12 @@ class TestMainLoop(unittest.TestCase):
os.write(pipe_w, b"Y")
os.close(pipe_w)
- def excepthook(type, value, traceback):
- self.assertTrue(type is Exception)
- self.assertEqual(value.args[0], "deadbabe")
- sys.excepthook = excepthook
- try:
- got_exception = False
- try:
- loop.run()
- except:
- got_exception = True
- finally:
- sys.excepthook = sys.__excepthook__
-
- #
- # The exception should be handled (by printing it)
- # immediately on return from child_died() rather
- # than here. See bug #303573
- #
- self.assertFalse(got_exception)
+ with capture_exceptions() as exc:
+ loop.run()
+
+ assert len(exc) == 1
+ assert exc[0].type is Exception
+ assert exc[0].value.args[0] == "deadbabe"
@unittest.skipUnless(hasattr(os, "fork"), "no os.fork available")
def test_sigint(self):
diff --git a/tests/test_option.py b/tests/test_option.py
index 33a12882..2854508b 100644
--- a/tests/test_option.py
+++ b/tests/test_option.py
@@ -3,7 +3,6 @@
from __future__ import absolute_import
import unittest
-import sys
# py3k has StringIO in a different module
try:
@@ -14,9 +13,10 @@ except ImportError:
from gi.repository import GLib
+from .helper import capture_exceptions
+
class TestOption(unittest.TestCase):
- EXCEPTION_MESSAGE = "This callback fails"
def setUp(self):
self.parser = GLib.option.OptionParser("NAMES...",
@@ -30,7 +30,7 @@ class TestOption(unittest.TestCase):
def _create_group(self):
def option_callback(option, opt, value, parser):
- raise Exception(self.EXCEPTION_MESSAGE)
+ raise Exception("foo")
group = GLib.option.OptionGroup(
"unittest", "Unit test options", "Show all unittest options",
@@ -104,14 +104,10 @@ class TestOption(unittest.TestCase):
def test_standard_error(self):
self._create_group()
- sio = StringIO()
- old_stderr = sys.stderr
- sys.stderr = sio
- try:
+
+ with capture_exceptions() as exc:
self.parser.parse_args(
["test_option.py", "--callback-failure-test"])
- finally:
- sys.stderr = old_stderr
- assert (sio.getvalue().split('\n')[-2] ==
- "Exception: " + self.EXCEPTION_MESSAGE)
+ assert len(exc) == 1
+ assert exc[0].value.args[0] == "foo"