Skip to content

Commit 1757720

Browse files
committed
Add twisted-embedded mode
An ability to use plugin inside threaded and repetative calls to pytest.main from a running twisted application.
1 parent c762733 commit 1757720

File tree

2 files changed

+79
-16
lines changed

2 files changed

+79
-16
lines changed

pytest_twisted.py

Lines changed: 27 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import greenlet, pytest
22
from twisted.internet import reactor, defer
3+
from twisted.internet.threads import blockingCallFromThread
34
from twisted.python import failure
45
from decorator import decorator
56

@@ -8,7 +9,8 @@
89

910
def blockon(d):
1011
current = greenlet.getcurrent()
11-
assert current is not gr_twisted, "blockon cannot be called from the twisted greenlet"
12+
assert current is not gr_twisted, \
13+
"blockon cannot be called from the twisted greenlet"
1214
result = []
1315

1416
def cb(r):
@@ -27,21 +29,18 @@ def cb(r):
2729
return result[0]
2830

2931

32+
def block_from_thread(d):
33+
return blockingCallFromThread(reactor, lambda x: x, d)
34+
35+
3036
@decorator
3137
def inlineCallbacks(fun, *args, **kw):
3238
return defer.inlineCallbacks(fun)(*args, **kw)
3339

3440

3541
def pytest_namespace():
36-
return dict(inlineCallbacks=inlineCallbacks, blockon=blockon)
37-
38-
39-
def init_twisted_greenlet():
40-
global gr_twisted
41-
if not gr_twisted:
42-
gr_twisted = greenlet.greenlet(reactor.run)
43-
failure.Failure.cleanFailure = lambda self: None # give me better tracebacks
44-
return gr_twisted
42+
return dict(inlineCallbacks=inlineCallbacks,
43+
blockon=block_from_thread if reactor.running else blockon)
4544

4645

4746
def stop_twisted_greenlet():
@@ -51,7 +50,11 @@ def stop_twisted_greenlet():
5150

5251

5352
def pytest_addhooks(pluginmanager):
54-
init_twisted_greenlet()
53+
global gr_twisted
54+
if not gr_twisted and not reactor.running:
55+
gr_twisted = greenlet.greenlet(reactor.run)
56+
# give me better tracebacks:
57+
failure.Failure.cleanFailure = lambda self: None
5558

5659

5760
@pytest.fixture(scope="session", autouse=True)
@@ -76,10 +79,18 @@ def _pytest_pyfunc_call(pyfuncitem):
7679

7780

7881
def pytest_pyfunc_call(pyfuncitem):
79-
if gr_twisted.dead:
80-
raise RuntimeError("twisted reactor has stopped")
82+
if gr_twisted is not None:
83+
if gr_twisted.dead:
84+
raise RuntimeError("twisted reactor has stopped")
8185

82-
d = defer.Deferred()
83-
reactor.callLater(0.0, lambda: defer.maybeDeferred(_pytest_pyfunc_call, pyfuncitem).chainDeferred(d))
84-
blockon(d)
86+
def in_reactor(d, f, *args):
87+
return defer.maybeDeferred(f, *args).chainDeferred(d)
88+
89+
d = defer.Deferred()
90+
reactor.callLater(0.0, in_reactor, d, _pytest_pyfunc_call, pyfuncitem)
91+
blockon(d)
92+
else:
93+
if not reactor.running:
94+
raise RuntimeError("twisted reactor is not running")
95+
blockingCallFromThread(reactor, _pytest_pyfunc_call, pyfuncitem)
8596
return True

testing/test_basic.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,3 +126,55 @@ def test_succeed():
126126
rr = testdir.run(sys.executable, "-m", "pytest", "-v")
127127
outcomes = rr.parseoutcomes()
128128
assert outcomes.get("passed") == 1
129+
130+
131+
def test_pytest_from_reactor_thread(testdir):
132+
testdir.makepyfile("""
133+
import pytest
134+
from twisted.internet import reactor, defer
135+
136+
@pytest.fixture
137+
def fix():
138+
d = defer.Deferred()
139+
reactor.callLater(0.01, d.callback, 42)
140+
return pytest.blockon(d)
141+
142+
def test_simple(fix):
143+
assert fix == 42
144+
145+
@pytest.inlineCallbacks
146+
def test_fail():
147+
d = defer.Deferred()
148+
reactor.callLater(0.01, d.callback, 1)
149+
yield d
150+
assert False
151+
""")
152+
testdir.makepyfile(runner="""
153+
import pytest
154+
155+
from twisted.internet import reactor
156+
from twisted.internet.defer import inlineCallbacks
157+
from twisted.internet.threads import deferToThread
158+
159+
codes = []
160+
161+
@inlineCallbacks
162+
def main():
163+
try:
164+
codes.append((yield deferToThread(pytest.main, ['-k simple'])))
165+
codes.append((yield deferToThread(pytest.main, ['-k fail'])))
166+
finally:
167+
reactor.stop()
168+
169+
if __name__ == '__main__':
170+
reactor.callLater(0, main)
171+
reactor.run()
172+
codes == [0, 1] or exit(1)
173+
""")
174+
# check test file is ok in standalone mode:
175+
rr = testdir.run(sys.executable, "-m", "pytest", "-v")
176+
outcomes = rr.parseoutcomes()
177+
assert outcomes.get("passed") == 1
178+
assert outcomes.get("failed") == 1
179+
# test embedded mode:
180+
assert testdir.run(sys.executable, "runner.py").ret == 0

0 commit comments

Comments
 (0)