From af406a600e8d3a871adb95afc7d7505bfbb4ddda Mon Sep 17 00:00:00 2001 From: Brian Warner Date: Wed, 14 Feb 2018 00:40:18 -0800 Subject: [PATCH] _rlcompleter: use blockingCallFromThread for all Helper APIs We were missing two (the calls to choose_nameplate() and choose_words() that happen after the input() function has finished, but while we're still inside the thread that makes it safe for input() to block). This almost certainly caused the crash seen in issue #280. Update the tests to match: CodeInputter.finish must now be called with deferToThread from inside tests, or the internal blockingCallFromThread must be stubbed out. --- src/wormhole/_rlcompleter.py | 6 ++++-- src/wormhole/test/test_rlcompleter.py | 10 ++++++++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/wormhole/_rlcompleter.py b/src/wormhole/_rlcompleter.py index 95288dc..c717ed1 100644 --- a/src/wormhole/_rlcompleter.py +++ b/src/wormhole/_rlcompleter.py @@ -134,11 +134,13 @@ class CodeInputter(object): raise AlreadyInputNameplateError("nameplate (%s-) already entered, cannot go back" % self._committed_nameplate) else: debug(" choose_nameplate(%s)" % nameplate) - self._input_helper.choose_nameplate(nameplate) + self.bcft(self._input_helper.choose_nameplate, nameplate) debug(" choose_words(%s)" % words) - self._input_helper.choose_words(words) + self.bcft(self._input_helper.choose_words, words) def _input_code_with_completion(prompt, input_helper, reactor): + # reminder: this all occurs in a separate thread. All calls to input_helper + # must go through blockingCallFromThread() c = CodeInputter(input_helper, reactor) if readline is not None: if readline.__doc__ and "libedit" in readline.__doc__: diff --git a/src/wormhole/test/test_rlcompleter.py b/src/wormhole/test/test_rlcompleter.py index f21e55f..41f0abf 100644 --- a/src/wormhole/test/test_rlcompleter.py +++ b/src/wormhole/test/test_rlcompleter.py @@ -147,11 +147,15 @@ def get_completions(c, prefix): return completions completions.append(text) +def fake_blockingCallFromThread(f, *a, **kw): + return f(*a, **kw) + class Completion(unittest.TestCase): def test_simple(self): # no actual completion helper = mock.Mock() c = CodeInputter(helper, "reactor") + c.bcft = fake_blockingCallFromThread c.finish("1-code-ghost") self.assertFalse(c.used_completion) self.assertEqual(helper.mock_calls, @@ -164,6 +168,7 @@ class Completion(unittest.TestCase): # check that it calls _commit_and_build_completions correctly helper = mock.Mock() c = CodeInputter(helper, "reactor") + c.bcft = fake_blockingCallFromThread # pretend nameplates: 1, 12, 34 @@ -304,12 +309,13 @@ class Completion(unittest.TestCase): self.assertEqual(gwc.mock_calls, [mock.call("and-b")]) gwc.reset_mock() - c.finish("12-and-bat") + yield deferToThread(c.finish, "12-and-bat") self.assertEqual(cw.mock_calls, [mock.call("and-bat")]) def test_incomplete_code(self): helper = mock.Mock() c = CodeInputter(helper, "reactor") + c.bcft = fake_blockingCallFromThread with self.assertRaises(KeyFormatError) as e: c.finish("1") self.assertEqual(str(e.exception), "incomplete wormhole code") @@ -349,7 +355,7 @@ class Completion(unittest.TestCase): self.assertEqual(matches, ["1-code", "1-court"]) helper.reset_mock() with self.assertRaises(AlreadyInputNameplateError) as e: - c.finish("2-code") + yield deferToThread(c.finish, "2-code") self.assertEqual(str(e.exception), "nameplate (1-) already entered, cannot go back") self.assertEqual(helper.mock_calls, [])