From f6be25bc73505d0a1024751cfa201804ad511c89 Mon Sep 17 00:00:00 2001 From: Tobi-wan Kenobi Date: Sat, 17 Dec 2016 07:43:38 +0100 Subject: [PATCH] [core/input] Move from select to epoll Use epoll instead of select in order to be able to use level-triggered semantics and not get stuck on the first event. --- bumblebee/input.py | 29 ++++++++++++++------------ bumblebee/modules/dnf.py | 2 +- tests/modules/test_brightness.py | 6 +++--- tests/modules/test_cmus.py | 7 ++++--- tests/modules/test_cpu.py | 2 +- tests/modules/test_disk.py | 6 +++--- tests/modules/test_load.py | 2 +- tests/modules/test_memory.py | 2 +- tests/modules/test_pulseaudio.py | 8 ++++---- tests/test_i3barinput.py | 35 ++++++++++++++++---------------- tests/util.py | 16 ++++++++++++++- 11 files changed, 67 insertions(+), 48 deletions(-) diff --git a/bumblebee/input.py b/bumblebee/input.py index dc2e723..b9389c2 100644 --- a/bumblebee/input.py +++ b/bumblebee/input.py @@ -16,24 +16,27 @@ WHEEL_DOWN = 5 def read_input(inp): """Read i3bar input and execute callbacks""" + epoll = select.epoll() + epoll.register(sys.stdin.fileno(), select.EPOLLIN) while inp.running: for thread in threading.enumerate(): if thread.name == "MainThread" and not thread.is_alive(): return - rlist, _, _ = select.select([sys.stdin], [], [], 1) - if not rlist: - continue - line = sys.stdin.readline().strip(",").strip() - inp.has_event = True - try: - event = json.loads(line) - if not "instance" in event: - continue - inp.callback(event) - inp.redraw() - except ValueError: - pass + events = epoll.poll(1) + + for fileno, event in events: + line = sys.stdin.readline().strip(",").strip() + inp.has_event = True + try: + event = json.loads(line) + if "instance" in event: + inp.callback(event) + inp.redraw() + except ValueError: + pass + epoll.unregister(sys.stdin.fileno()) + epoll.close() inp.has_event = True inp.clean_exit = True diff --git a/bumblebee/modules/dnf.py b/bumblebee/modules/dnf.py index f6acf2a..2cb249b 100644 --- a/bumblebee/modules/dnf.py +++ b/bumblebee/modules/dnf.py @@ -25,7 +25,7 @@ def get_dnf_info(widget): bugfixes = 0 enhancements = 0 other = 0 - for line in res.decode().split("\n"): + for line in res.split("\n"): if not line.startswith(" "): continue elif "ecurity" in line: diff --git a/tests/modules/test_brightness.py b/tests/modules/test_brightness.py index 0594fc3..b14af4b 100644 --- a/tests/modules/test_brightness.py +++ b/tests/modules/test_brightness.py @@ -24,7 +24,7 @@ class TestBrightnessModule(unittest.TestCase): for widget in self.module.widgets(): self.assertEquals(len(widget.full_text()), len("100%")) - @mock.patch("select.select") + @mock.patch("select.epoll") @mock.patch("subprocess.Popen") @mock.patch("sys.stdin") def test_wheel_up(self, mock_input, mock_output, mock_select): @@ -33,7 +33,7 @@ class TestBrightnessModule(unittest.TestCase): "xbacklight +2%" ) - @mock.patch("select.select") + @mock.patch("select.epoll") @mock.patch("subprocess.Popen") @mock.patch("sys.stdin") def test_wheel_down(self, mock_input, mock_output, mock_select): @@ -42,7 +42,7 @@ class TestBrightnessModule(unittest.TestCase): "xbacklight -2%" ) - @mock.patch("select.select") + @mock.patch("select.epoll") @mock.patch("subprocess.Popen") @mock.patch("sys.stdin") def test_custom_step(self, mock_input, mock_output, mock_select): diff --git a/tests/modules/test_cmus.py b/tests/modules/test_cmus.py index 1bc8b67..58be4bc 100644 --- a/tests/modules/test_cmus.py +++ b/tests/modules/test_cmus.py @@ -7,7 +7,7 @@ import mock import bumblebee.input from bumblebee.input import I3BarInput from bumblebee.modules.cmus import Module -from tests.util import MockEngine, MockConfig, assertPopen +from tests.util import MockEngine, MockConfig, assertPopen, MockEpoll class TestCmusModule(unittest.TestCase): def setUp(self): @@ -29,7 +29,7 @@ class TestCmusModule(unittest.TestCase): def test_widgets(self): self.assertTrue(len(self.module.widgets()), 5) - @mock.patch("select.select") + @mock.patch("select.epoll") @mock.patch("subprocess.Popen") @mock.patch("sys.stdin") def test_interaction(self, mock_input, mock_output, mock_select): @@ -41,7 +41,8 @@ class TestCmusModule(unittest.TestCase): {"widget": "cmus.main", "action": "cmus-remote -u"}, ] - mock_select.return_value = (1,2,3) + mock_input.fileno.return_value = 1 + mock_select.return_value = MockEpoll() for event in events: mock_input.readline.return_value = json.dumps({ diff --git a/tests/modules/test_cpu.py b/tests/modules/test_cpu.py index a00d2c2..fea056e 100644 --- a/tests/modules/test_cpu.py +++ b/tests/modules/test_cpu.py @@ -22,7 +22,7 @@ class TestCPUModule(unittest.TestCase): for widget in self.module.widgets(): self.assertEquals(len(widget.full_text()), len("100.00%")) - @mock.patch("select.select") + @mock.patch("select.epoll") @mock.patch("subprocess.Popen") @mock.patch("sys.stdin") def test_leftclick(self, mock_input, mock_output, mock_select): diff --git a/tests/modules/test_disk.py b/tests/modules/test_disk.py index f8fee4f..423612e 100644 --- a/tests/modules/test_disk.py +++ b/tests/modules/test_disk.py @@ -7,7 +7,7 @@ import mock import bumblebee.input from bumblebee.input import I3BarInput from bumblebee.modules.disk import Module -from tests.util import MockEngine, MockConfig, assertPopen, assertStateContains +from tests.util import MockEngine, MockConfig, assertPopen, assertStateContains, MockEpoll class MockVFS(object): def __init__(self, perc): @@ -24,7 +24,7 @@ class TestDiskModule(unittest.TestCase): self.config.set("disk.path", "somepath") self.module = Module(engine=self.engine, config={"config": self.config}) - @mock.patch("select.select") + @mock.patch("select.epoll") @mock.patch("subprocess.Popen") @mock.patch("sys.stdin") def test_leftclick(self, mock_input, mock_output, mock_select): @@ -33,7 +33,7 @@ class TestDiskModule(unittest.TestCase): "button": bumblebee.input.LEFT_MOUSE, "instance": None }) - mock_select.return_value = (1,2,3) + mock_select.return_value = MockEpoll() self.engine.input.start() self.engine.input.stop() mock_input.readline.assert_any_call() diff --git a/tests/modules/test_load.py b/tests/modules/test_load.py index d8120e2..e4ed353 100644 --- a/tests/modules/test_load.py +++ b/tests/modules/test_load.py @@ -17,7 +17,7 @@ class TestLoadModule(unittest.TestCase): self.config = MockConfig() self.module = Module(engine=self.engine, config={ "config": self.config }) - @mock.patch("select.select") + @mock.patch("select.epoll") @mock.patch("subprocess.Popen") @mock.patch("sys.stdin") def test_leftclick(self, mock_input, mock_output, mock_select): diff --git a/tests/modules/test_memory.py b/tests/modules/test_memory.py index 4533357..b32205d 100644 --- a/tests/modules/test_memory.py +++ b/tests/modules/test_memory.py @@ -21,7 +21,7 @@ class TestMemoryModule(unittest.TestCase): self.config = MockConfig() self.module = Module(engine=self.engine, config={ "config": self.config }) - @mock.patch("select.select") + @mock.patch("select.epoll") @mock.patch("subprocess.Popen") @mock.patch("sys.stdin") def test_leftclick(self, mock_input, mock_output, mock_select): diff --git a/tests/modules/test_pulseaudio.py b/tests/modules/test_pulseaudio.py index 6515589..5aae761 100644 --- a/tests/modules/test_pulseaudio.py +++ b/tests/modules/test_pulseaudio.py @@ -17,7 +17,7 @@ class TestPulseAudioModule(unittest.TestCase): self.config = MockConfig() self.module = Module(engine=self.engine, config={ "config": self.config }) - @mock.patch("select.select") + @mock.patch("select.epoll") @mock.patch("subprocess.Popen") @mock.patch("sys.stdin") def test_leftclick(self, mock_input, mock_output, mock_select): @@ -26,7 +26,7 @@ class TestPulseAudioModule(unittest.TestCase): "pactl set-source-mute @DEFAULT_SOURCE@ toggle" ) - @mock.patch("select.select") + @mock.patch("select.epoll") @mock.patch("subprocess.Popen") @mock.patch("sys.stdin") def test_rightclick(self, mock_input, mock_output, mock_select): @@ -35,7 +35,7 @@ class TestPulseAudioModule(unittest.TestCase): "pavucontrol" ) - @mock.patch("select.select") + @mock.patch("select.epoll") @mock.patch("subprocess.Popen") @mock.patch("sys.stdin") def test_wheelup(self, mock_input, mock_output, mock_select): @@ -44,7 +44,7 @@ class TestPulseAudioModule(unittest.TestCase): "pactl set-source-volume @DEFAULT_SOURCE@ +2%" ) - @mock.patch("select.select") + @mock.patch("select.epoll") @mock.patch("subprocess.Popen") @mock.patch("sys.stdin") def test_wheeldown(self, mock_input, mock_output, mock_select): diff --git a/tests/test_i3barinput.py b/tests/test_i3barinput.py index abe972f..ca9892f 100644 --- a/tests/test_i3barinput.py +++ b/tests/test_i3barinput.py @@ -7,7 +7,7 @@ import mock import bumblebee.input from bumblebee.input import I3BarInput -from tests.util import MockWidget, MockModule, assertPopen, assertMouseEvent +from tests.util import MockWidget, MockModule, assertPopen, assertMouseEvent, MockEpoll class TestI3BarInput(unittest.TestCase): def setUp(self): @@ -21,29 +21,30 @@ class TestI3BarInput(unittest.TestCase): def callback(self, event): self._called += 1 - @mock.patch("select.select") + @mock.patch("select.epoll") @mock.patch("sys.stdin") def test_basic_read_event(self, mock_input, mock_select): - mock_select.return_value = (1,2,3) - mock_input.readline.return_value = "" + mock_input.readline.return_value = "somedata" + mock_input.fileno.return_value = 1 + mock_select.return_value = MockEpoll() self.input.start() self.input.stop() mock_input.readline.assert_any_call() - @mock.patch("select.select") + @mock.patch("select.epoll") @mock.patch("sys.stdin") def test_ignore_invalid_data(self, mock_input, mock_select): - mock_select.return_value = (1,2,3) + mock_select.return_value = MockEpoll() mock_input.readline.return_value = "garbage" self.input.start() self.assertEquals(self.input.alive(), True) self.assertEquals(self.input.stop(), True) mock_input.readline.assert_any_call() - @mock.patch("select.select") + @mock.patch("select.epoll") @mock.patch("sys.stdin") def test_ignore_invalid_event(self, mock_input, mock_select): - mock_select.return_value = (1,2,3) + mock_select.return_value = MockEpoll() mock_input.readline.return_value = json.dumps({ "name": None, "instance": None, @@ -54,10 +55,10 @@ class TestI3BarInput(unittest.TestCase): self.assertEquals(self.input.stop(), True) mock_input.readline.assert_any_call() - @mock.patch("select.select") + @mock.patch("select.epoll") @mock.patch("sys.stdin") def test_ignore_partial_event(self, mock_input, mock_select): - mock_select.return_value = (1,2,3) + mock_select.return_value = MockEpoll() self.input.register_callback(None, button=1, cmd=self.callback) mock_input.readline.return_value = json.dumps({ "button": 1, @@ -67,7 +68,7 @@ class TestI3BarInput(unittest.TestCase): self.assertEquals(self.input.stop(), True) mock_input.readline.assert_any_call() - @mock.patch("select.select") + @mock.patch("select.epoll") @mock.patch("sys.stdin") def test_global_callback(self, mock_input, mock_select): self.input.register_callback(None, button=1, cmd=self.callback) @@ -75,7 +76,7 @@ class TestI3BarInput(unittest.TestCase): bumblebee.input.LEFT_MOUSE, None, "someinstance") self.assertTrue(self._called > 0) - @mock.patch("select.select") + @mock.patch("select.epoll") @mock.patch("sys.stdin") def test_remove_global_callback(self, mock_input, mock_select): self.input.register_callback(None, button=1, cmd=self.callback) @@ -84,7 +85,7 @@ class TestI3BarInput(unittest.TestCase): bumblebee.input.LEFT_MOUSE, None, "someinstance") self.assertTrue(self._called == 0) - @mock.patch("select.select") + @mock.patch("select.epoll") @mock.patch("sys.stdin") def test_global_callback_button_missmatch(self, mock_input, mock_select): self.input.register_callback(self.anyModule, button=1, cmd=self.callback) @@ -92,7 +93,7 @@ class TestI3BarInput(unittest.TestCase): bumblebee.input.RIGHT_MOUSE, None, "someinstance") self.assertTrue(self._called == 0) - @mock.patch("select.select") + @mock.patch("select.epoll") @mock.patch("sys.stdin") def test_module_callback(self, mock_input, mock_select): self.input.register_callback(self.anyModule, button=1, cmd=self.callback) @@ -100,7 +101,7 @@ class TestI3BarInput(unittest.TestCase): bumblebee.input.LEFT_MOUSE, None) self.assertTrue(self._called > 0) - @mock.patch("select.select") + @mock.patch("select.epoll") @mock.patch("sys.stdin") def test_remove_module_callback(self, mock_input, mock_select): self.input.register_callback(self.anyModule, button=1, cmd=self.callback) @@ -109,7 +110,7 @@ class TestI3BarInput(unittest.TestCase): bumblebee.input.LEFT_MOUSE, None, self.anyWidget.id) self.assertTrue(self._called == 0) - @mock.patch("select.select") + @mock.patch("select.epoll") @mock.patch("sys.stdin") def test_widget_callback(self, mock_input, mock_select): self.input.register_callback(self.anyWidget, button=1, cmd=self.callback) @@ -117,7 +118,7 @@ class TestI3BarInput(unittest.TestCase): bumblebee.input.LEFT_MOUSE, None, self.anyWidget.id) self.assertTrue(self._called > 0) - @mock.patch("select.select") + @mock.patch("select.epoll") @mock.patch("subprocess.Popen") @mock.patch("sys.stdin") def test_widget_cmd_callback(self, mock_input, mock_output, mock_select): diff --git a/tests/util.py b/tests/util.py index 4b599b7..c0f244d 100644 --- a/tests/util.py +++ b/tests/util.py @@ -23,13 +23,27 @@ def assertStateContains(test, module, state): module.update(module.widgets()) test.assertTrue(state in module.widgets()[0].state()) +class MockEpoll(object): + def register(self, fileno, event): + pass + + def poll(self, timeout): + return [(1,2)] + + def unregister(self, fileno): + pass + + def close(self): + pass + def assertMouseEvent(mock_input, mock_output, mock_select, engine, module, button, cmd, instance_id=None): mock_input.readline.return_value = json.dumps({ "name": module.id if module else "test", "button": button, "instance": instance_id }) - mock_select.return_value = (1, 2, 3) + mock_input.fileno.return_value = 1 + mock_select.return_value = MockEpoll() engine.input.start() engine.input.stop() mock_input.readline.assert_any_call()