diff --git a/tests/test.py b/tests/test.py index 1284a498..4d635467 100644 --- a/tests/test.py +++ b/tests/test.py @@ -22,6 +22,7 @@ """Sets up key-mapper for the tests and runs them.""" +import os import sys import time import unittest @@ -34,6 +35,11 @@ import evdev from keymapper.logger import update_verbosity +assert not os.getcwd().endswith('tests') + +sys.path = [os.path.abspath('.')] + sys.path + + tmp = '/tmp/key-mapper-test' uinput_write_history = [] # for tests that makes the injector create its processes diff --git a/tests/testcases/integration.py b/tests/testcases/integration.py index 529da6b0..b78cc1b4 100644 --- a/tests/testcases/integration.py +++ b/tests/testcases/integration.py @@ -74,7 +74,7 @@ class FakeDropdown(Gtk.ComboBoxText): return self.name -class Integration(unittest.TestCase): +class TestIntegration(unittest.TestCase): """For tests that use the window. Try to modify the configuration only by calling functions of the window. @@ -147,18 +147,38 @@ class Integration(unittest.TestCase): self.assertIsNotNone(self.window) self.assertTrue(self.window.window.get_visible()) - def test_adds_empty_rows(self): - rows = len(self.window.get('key_list').get_children()) - self.assertEqual(rows, 1) + def test_row_simple(self): + rows = self.window.get('key_list').get_children() + self.assertEqual(len(rows), 1) + + row = rows[0] + + row.set_new_keycode(None) + self.assertIsNone(row.get_keycode()) + self.assertEqual(len(custom_mapping), 0) + row.set_new_keycode(30) + self.assertEqual(len(custom_mapping), 0) + self.assertEqual(row.get_keycode(), 30) + row.set_new_keycode(30) + self.assertEqual(len(custom_mapping), 0) + self.assertEqual(row.get_keycode(), 30) - custom_mapping.change(13, 'a', None) time.sleep(0.2) gtk_iteration() + self.assertEqual(len(self.window.get('key_list').get_children()), 1) - rows = len(self.window.get('key_list').get_children()) - self.assertEqual(rows, 2) + row.character_input.set_text('Shift_L') + self.assertEqual(len(custom_mapping), 1) - def change_empty_row(self, keycode, character): + time.sleep(0.2) + gtk_iteration() + self.assertEqual(len(self.window.get('key_list').get_children()), 2) + + self.assertEqual(custom_mapping.get_keycode('Shift_L'), 30) + self.assertEqual(row.get_character(), 'Shift_L') + self.assertEqual(row.get_keycode(), 30) + + def change_empty_row(self, code, char, code_first=True, success=True): """Modify the one empty row that always exists.""" # wait for the window to create a new empty row if needed time.sleep(0.2) @@ -168,32 +188,53 @@ class Integration(unittest.TestCase): rows = self.get_rows() row = rows[-1] self.assertNotIn('changed', row.get_style_context().list_classes()) - self.assertIsNone(row.keycode.get_label()) + self.assertIsNone(row.keycode_input.get_label()) self.assertEqual(row.character_input.get_text(), '') - self.window.window.set_focus(row.keycode) - - pending_events[self.window.selected_device] = [ - Event(evdev.events.EV_KEY, keycode - 8, 1) - ] - - self.window.on_window_event(None, None) - - self.assertEqual(int(row.keycode.get_label()), keycode) - - # set the character to make the new row complete - row.character_input.set_text(character) - - self.assertIn('changed', row.get_style_context().list_classes()) + if char and not code_first: + # set the character to make the new row complete + self.assertIsNone(row.get_character()) + row.character_input.set_text(char) + self.assertEqual(row.get_character(), char) + + self.window.window.set_focus(row.keycode_input) + + if code: + # modifies the keycode in the row not by writing into the input, + # but by sending an event + pending_events[self.window.selected_device] = [ + Event(evdev.events.EV_KEY, code - 8, 1) + ] + self.window.on_window_event(None, None) + + if success: + self.assertEqual(row.get_keycode(), code) + self.assertIn( + 'changed', + row.get_style_context().list_classes() + ) + + if not success: + self.assertIsNone(row.get_keycode()) + self.assertIsNone(row.get_character()) + self.assertNotIn('changed', row.get_style_context().list_classes()) + + if char and code_first: + # set the character to make the new row complete + self.assertIsNone(row.get_character()) + row.character_input.set_text(char) + self.assertEqual(row.get_character(), char) return row def test_rows(self): """Comprehensive test for rows.""" + # how many rows there should be in the end + num_rows_target = 3 # add two rows by modifiying the one empty row that exists - self.change_empty_row(10, 'a') - self.change_empty_row(11, 'b') + self.change_empty_row(10, 'a', code_first=False) + self.change_empty_row(11, 'k(b).k(c)') # one empty row added automatically again time.sleep(0.2) @@ -201,10 +242,10 @@ class Integration(unittest.TestCase): # sleep one more time because it's funny to watch the ui # during the test, how rows turn blue and stuff time.sleep(0.2) - self.assertEqual(len(self.get_rows()), 3) + self.assertEqual(len(self.get_rows()), num_rows_target) self.assertEqual(custom_mapping.get_character(10), 'a') - self.assertEqual(custom_mapping.get_character(11), 'b') + self.assertEqual(custom_mapping.get_character(11), 'k(b).k(c)') self.assertTrue(custom_mapping.changed) self.window.on_save_preset_clicked(None) @@ -227,9 +268,54 @@ class Integration(unittest.TestCase): ) self.assertEqual(custom_mapping.get_character(10), 'c') - self.assertEqual(custom_mapping.get_character(11), 'b') + self.assertEqual(custom_mapping.get_character(11), 'k(b).k(c)') self.assertTrue(custom_mapping.changed) + # try to add a duplicate keycode, it should be ignored + self.change_empty_row(11, 'd', success=False) + self.assertEqual(custom_mapping.get_character(11), 'k(b).k(c)') + self.assertIsNone(custom_mapping.get_keycode('d')) + # and the number of rows shouldn't change + self.assertEqual(len(self.get_rows()), num_rows_target) + + def test_remove_row(self): + """Comprehensive test for rows 2.""" + # sleeps are added to be able to visually follow and debug the test + # add two rows by modifiying the one empty row that exists + row_1 = self.change_empty_row(10, 'a') + row_2 = self.change_empty_row(11, 'b') + row_3 = self.change_empty_row(None, 'c') + + # no empty row added because one is unfinished + time.sleep(0.2) + gtk_iteration() + self.assertEqual(len(self.get_rows()), 3) + + self.assertEqual(custom_mapping.get_character(11), 'b') + self.assertEqual(custom_mapping.get_keycode('b'), 11) + + def remove(row, code, char, num_rows_after): + if code is not None and char is not None: + self.assertEqual(custom_mapping.get_character(code), char) + self.assertEqual(custom_mapping.get_keycode(char), code) + self.assertEqual(row.get_character(), char) + self.assertEqual(row.get_keycode(), code) + row.on_delete_button_clicked() + time.sleep(0.2) + gtk_iteration() + self.assertIsNone(row.get_keycode()) + self.assertIsNone(row.get_character()) + self.assertIsNone(custom_mapping.get_keycode(char)) + self.assertIsNone(custom_mapping.get_character(code)) + self.assertEqual(len(self.get_rows()), num_rows_after) + + remove(row_1, 10, 'a', 2) + remove(row_2, 11, 'b', 1) + # there is no empty row at the moment, so after removing that one, + # which is the only row, one empty row will be there. So the number + # of rows won't change. + remove(row_3, None, 'c', 1) + def test_rename_and_save(self): custom_mapping.change(14, 'a', None) self.assertEqual(self.window.selected_preset, 'new preset') diff --git a/tests/testcases/logger.py b/tests/testcases/logger.py new file mode 100644 index 00000000..f0d12f25 --- /dev/null +++ b/tests/testcases/logger.py @@ -0,0 +1,130 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +# key-mapper - GUI for device specific keyboard mappings +# Copyright (C) 2020 sezanzeb +# +# This file is part of key-mapper. +# +# key-mapper is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# key-mapper is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with key-mapper. If not, see . + + +import os +import shutil +import unittest +import logging + +from keymapper.logger import logger, add_filehandler, update_verbosity, \ + log_info + +from test import tmp + + +class TestLogger(unittest.TestCase): + def tearDown(self): + # remove the file handler + logger.handlers = [ + handler for handler in logger.handlers + if not isinstance(logger.handlers, logging.FileHandler) + ] + path = os.path.join(tmp, 'logger-test') + if os.path.exists(path): + shutil.rmtree(path) + + update_verbosity(debug=True) + + def test_log_info(self): + update_verbosity(debug=False) + path = add_filehandler(os.path.join(tmp, 'logger-test')) + log_info() + with open(path, 'r') as f: + content = f.read().lower() + self.assertIn('key-mapper', content) + + def test_makes_path(self): + path = os.path.join(tmp, 'logger-test') + if os.path.exists(path): + shutil.rmtree(path) + + new_path = os.path.join(tmp, 'logger-test', 'a', 'b', 'c') + add_filehandler(new_path) + self.assertTrue(os.path.exists(new_path)) + + def test_clears_log(self): + path = os.path.join(tmp, 'logger-test', 'log') + os.makedirs(os.path.dirname(path), exist_ok=True) + os.mknod(path) + with open(path, 'w') as f: + f.write('foo') + add_filehandler(os.path.join(tmp, 'logger-test')) + with open(path, 'r') as f: + self.assertEqual(f.read(), '') + + def test_debug(self): + path = add_filehandler(os.path.join(tmp, 'logger-test')) + logger.error('abc') + logger.warn('foo') + logger.info('123') + logger.debug('456') + logger.spam('789') + with open(path, 'r') as f: + content = f.read().lower() + self.assertIn('logger.py', content) + self.assertIn('line', content) + + self.assertIn('error', content) + self.assertIn('abc', content) + + self.assertIn('warn', content) + self.assertIn('foo', content) + + self.assertIn('info', content) + self.assertIn('123', content) + + self.assertIn('debug', content) + self.assertIn('456', content) + + self.assertIn('spam', content) + self.assertIn('789', content) + + def test_default(self): + path = add_filehandler(os.path.join(tmp, 'logger-test')) + update_verbosity(debug=False) + logger.error('abc') + logger.warn('foo') + logger.info('123') + logger.debug('456') + logger.spam('789') + with open(path, 'r') as f: + content = f.read().lower() + self.assertNotIn('logger.py', content) + self.assertNotIn('line', content) + + self.assertIn('error', content) + self.assertIn('abc', content) + + self.assertIn('warn', content) + self.assertIn('foo', content) + + self.assertNotIn('info', content) + self.assertIn('123', content) + + self.assertNotIn('debug', content) + self.assertNotIn('456', content) + + self.assertNotIn('spam', content) + self.assertNotIn('789', content) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/testcases/permissions.py b/tests/testcases/permissions.py new file mode 100644 index 00000000..9003c9e4 --- /dev/null +++ b/tests/testcases/permissions.py @@ -0,0 +1,54 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +# key-mapper - GUI for device specific keyboard mappings +# Copyright (C) 2020 sezanzeb +# +# This file is part of key-mapper. +# +# key-mapper is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# key-mapper is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with key-mapper. If not, see . + + +import os +import grp +import unittest + +from keymapper.dev.permissions import can_read_devices + + +class TestPermissions(unittest.TestCase): + def setUp(self): + self.getgrnam = grp.getgrnam + + def tearDown(self): + grp.getgrnam = self.getgrnam + + def test_cannot_access(self): + class Grnam: + def __init__(self, group): + self.gr_mem = [] + + grp.getgrnam = Grnam + self.assertFalse(can_read_devices()) + + def test_can_access(self): + class Grnam: + def __init__(self, group): + self.gr_mem = [os.getlogin()] + + grp.getgrnam = Grnam + self.assertTrue(can_read_devices()) + + +if __name__ == "__main__": + unittest.main()