diff --git a/checkbox-ng/mk-venv b/checkbox-ng/mk-venv index 981341fd1f..9a6bd41dd2 100755 --- a/checkbox-ng/mk-venv +++ b/checkbox-ng/mk-venv @@ -15,7 +15,7 @@ virtualenv --quiet --system-site-packages --python=python3 "$venv_path" # shellcheck source=/dev/null . "$venv_path"/bin/activate python3 -m pip install -e . -pip install tqdm psutil +pip install tqdm psutil evdev mkdir -p "$venv_path/share/plainbox-providers-1" echo "export PROVIDERPATH=$venv_path/share/plainbox-providers-1" >> "$venv_path"/bin/activate diff --git a/providers/base/bin/mouse_keyboard.py b/providers/base/bin/mouse_keyboard.py new file mode 100755 index 0000000000..ac9ce0a3b8 --- /dev/null +++ b/providers/base/bin/mouse_keyboard.py @@ -0,0 +1,161 @@ +#!/usr/bin/env python3 +# +# This file is part of Checkbox. +# +# Copyright 2025 Canonical Ltd. +# +# Authors: +# Gabriel Chen +# +# Checkbox is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 3, +# as published by the Free Software Foundation. +# +# Checkbox 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 Checkbox. If not, see . +"""mouse_key_random utility.""" + +import time +import random +from evdev import UInput, ecodes as e + +# Constants +FREQUENCY_USEC = 100000 # Frequency of events in microseconds +N_EPISODES = 81 # Number of events to generate +WEIGHT_MOUSEMOVE = 10 # Weight of mouse movements +WEIGHT_KEYPRESS = 1 # Weight of key presses +WEIGHT_SUM = WEIGHT_MOUSEMOVE + WEIGHT_KEYPRESS # Total weight + +MOVE_MAX = 100 # Maximum mouse movement distance +MOVE_DELTA = 5 # Mouse movement step size + + +# Define keyboard keys and mouse buttons +KEYBOARD_KEYS = [ + e.KEY_A, + e.KEY_B, + e.KEY_C, + e.KEY_D, + e.KEY_E, + e.KEY_F, + e.KEY_G, + e.KEY_H, + e.KEY_I, + e.KEY_J, + e.KEY_K, + e.KEY_L, + e.KEY_M, + e.KEY_N, + e.KEY_O, + e.KEY_P, + e.KEY_Q, + e.KEY_R, + e.KEY_S, + e.KEY_T, + e.KEY_U, + e.KEY_V, + e.KEY_W, + e.KEY_X, + e.KEY_Y, + e.KEY_Z, + e.KEY_1, + e.KEY_2, + e.KEY_3, + e.KEY_4, + e.KEY_5, + e.KEY_6, + e.KEY_7, + e.KEY_8, + e.KEY_9, + e.KEY_0, +] +MOUSE_BUTTONS = [e.BTN_LEFT, e.BTN_RIGHT] + + +# Initialize the virtual input device +def dev_init(name): + # Define the capabilities of the device (keyboard and mouse events) + capabilities = { + e.EV_KEY: KEYBOARD_KEYS + MOUSE_BUTTONS, + e.EV_REL: [e.REL_X, e.REL_Y], + } + # Create the virtual input device + device = UInput( + capabilities, name=name, vendor=0xBAD, product=0xA55, version=777 + ) + time.sleep(1) # Give userspace time to detect the new device + return device + + +# Destroy the virtual input device +def dev_deinit(device): + time.sleep(1) # Give userspace time to read the remaining events + device.close() + + +# Simulate a key press +def key_press(device, key): + device.write(e.EV_KEY, key, 1) # Press the key + device.write(e.EV_KEY, key, 0) # Release the key + device.syn() # Synchronize the event + + +# Simulate mouse movement +def mouse_move(device, x, y): + device.write(e.EV_REL, e.REL_X, x) # Move mouse on the X axis + device.write(e.EV_REL, e.REL_Y, y) # Move mouse on the Y axis + device.syn() # Synchronize the event + + +# Randomly press a key +def rand_key_press(device): + key = random.choice(KEYBOARD_KEYS) # Choose a random key + key_press(device, key) # Simulate the key press + time.sleep(FREQUENCY_USEC / 1000000.0) # Wait for the defined frequency + + +# Randomly move the mouse +def rand_mouse_moves(device): + # Generate random X and Y movements + x = random.randint(-MOVE_MAX // 2, MOVE_MAX // 2) + y = random.randint(-MOVE_MAX // 2, MOVE_MAX // 2) + steps = ( + max(abs(x), abs(y)) // MOVE_DELTA + 1 + ) # Calculate the number of steps + # Move the mouse in small steps for smooth movement + for _ in range(steps): + mouse_move(device, x // steps, y // steps) + time.sleep(FREQUENCY_USEC / 1000000.0 / MOVE_DELTA) + + # Handle any remaining movement + rest_x = x % steps + rest_y = y % steps + if rest_x or rest_y: + mouse_move(device, rest_x, rest_y) + time.sleep(FREQUENCY_USEC / 1000000.0 / MOVE_DELTA) + + +def main(): + # Initialize the virtual input device + device = dev_init("umad") + random.seed(time.time()) # Seed the random number generator + + # Generate random events + for _ in range(N_EPISODES): + action = random.randint(0, WEIGHT_SUM - 1) # Choose a random action + if action < WEIGHT_MOUSEMOVE: + rand_mouse_moves(device) # Simulate mouse movement + else: + rand_key_press(device) # Simulate key press + + # Destroy the virtual input device + dev_deinit(device) + + +if __name__ == "__main__": + main() diff --git a/providers/base/debian/control b/providers/base/debian/control index 1d682e30be..c86d730295 100644 --- a/providers/base/debian/control +++ b/providers/base/debian/control @@ -37,6 +37,7 @@ Recommends: bonnie++, pm-utils, python3-apt, python3-dbus, + python3-evdev, python3-gi, smartmontools, sysstat, diff --git a/providers/base/tests/test_mouse_keyboard.py b/providers/base/tests/test_mouse_keyboard.py new file mode 100644 index 0000000000..0ef70bc0b9 --- /dev/null +++ b/providers/base/tests/test_mouse_keyboard.py @@ -0,0 +1,160 @@ +#!/usr/bin/env python3 +import unittest +from unittest.mock import MagicMock, patch +import time +import random +import evdev +from evdev import ecodes as e +from mouse_keyboard import ( + dev_init, + dev_deinit, + key_press, + mouse_move, + rand_key_press, + rand_mouse_moves, + main, + KEYBOARD_KEYS, + MOUSE_BUTTONS, + FREQUENCY_USEC, + MOVE_MAX, + MOVE_DELTA, + WEIGHT_MOUSEMOVE, + WEIGHT_KEYPRESS, + WEIGHT_SUM, + N_EPISODES, +) + + +class TestMouseKeyboard(unittest.TestCase): + + @patch("mouse_keyboard.UInput") + @patch("mouse_keyboard.time.sleep") + def test_dev_init(self, mock_sleep, mock_uinput): + # Mock the UInput class + mock_device = MagicMock() + mock_uinput.return_value = mock_device + + # Call the function + device = dev_init("umad") + + # Assertions + mock_uinput.assert_called_once_with( + { + e.EV_KEY: KEYBOARD_KEYS + MOUSE_BUTTONS, + e.EV_REL: [e.REL_X, e.REL_Y], + }, + name="umad", + vendor=0xBAD, + product=0xA55, + version=777, + ) + self.assertEqual(device, mock_device) + mock_sleep.assert_called_once_with(1) + + @patch("mouse_keyboard.time.sleep") + def test_dev_deinit(self, mock_sleep): + # Mock the device + mock_device = MagicMock() + + # Call the function + dev_deinit(mock_device) + + # Assertions + mock_sleep.assert_called_once_with(1) + mock_device.close.assert_called_once_with() + + def test_key_press(self): + # Mock the device + mock_device = MagicMock() + + # Call the function + key_press(mock_device, e.KEY_A) + + # Assertions + mock_device.write.assert_any_call(e.EV_KEY, e.KEY_A, 1) + mock_device.write.assert_any_call(e.EV_KEY, e.KEY_A, 0) + self.assertEqual(mock_device.syn.call_count, 1) + + def test_mouse_move(self): + # Mock the device + mock_device = MagicMock() + + # Call the function + mouse_move(mock_device, 10, 20) + + # Assertions + mock_device.write.assert_any_call(e.EV_REL, e.REL_X, 10) + mock_device.write.assert_any_call(e.EV_REL, e.REL_Y, 20) + mock_device.syn.assert_called_once_with() + + @patch("mouse_keyboard.random.choice") + @patch("mouse_keyboard.time.sleep") + def test_rand_key_press(self, mock_sleep, mock_choice): + # Mock the device and random choice + mock_device = MagicMock() + mock_choice.return_value = e.KEY_B + + # Call the function + rand_key_press(mock_device) + + # Assertions + mock_choice.assert_called_once_with(KEYBOARD_KEYS) + mock_device.write.assert_any_call(e.EV_KEY, e.KEY_B, 1) + mock_device.write.assert_any_call(e.EV_KEY, e.KEY_B, 0) + mock_sleep.assert_called_once_with(FREQUENCY_USEC / 1000000.0) + + @patch("mouse_keyboard.random.randint") + @patch("mouse_keyboard.time.sleep") + def test_rand_mouse_moves(self, mock_sleep, mock_randint): + # Mock the device and random.randint + mock_device = MagicMock() + mock_randint.side_effect = [50, -30] # x, y + + # Call the function + rand_mouse_moves(mock_device) + + # Assertions + self.assertEqual(mock_randint.call_count, 2) + mock_device.write.assert_any_call( + e.EV_REL, e.REL_X, 50 // (50 // MOVE_DELTA + 1) + ) + mock_device.write.assert_any_call( + e.EV_REL, e.REL_Y, -30 // (50 // MOVE_DELTA + 1) + ) + self.assertGreaterEqual(mock_sleep.call_count, 1) + + @patch("mouse_keyboard.time.time") + @patch("mouse_keyboard.rand_mouse_moves") + @patch("mouse_keyboard.random.seed") + @patch("mouse_keyboard.random.randint") + @patch("mouse_keyboard.dev_init") + @patch("mouse_keyboard.dev_deinit") + def test_main( + self, + mock_dev_deinit, + mock_dev_init, + mock_randint, + mock_seed, + mock_rand_mouse_moves, + mock_time, + ): + # Mock the device and random functions + mock_device = MagicMock() + mock_dev_init.return_value = mock_device + mock_randint.side_effect = [ + 0, + 10, + ] * N_EPISODES # Always choose mouse movement + + # Call the function + main() + + # Assertions + mock_seed.assert_called_once_with(mock_time()) + mock_dev_init.assert_called_once_with("umad") + self.assertEqual(mock_randint.call_count, N_EPISODES) + mock_dev_deinit.assert_called_once_with(mock_device) + + +if __name__ == "__main__": + unittest.main() diff --git a/providers/base/tox.ini b/providers/base/tox.ini index 3738a7f767..af6792fda3 100644 --- a/providers/base/tox.ini +++ b/providers/base/tox.ini @@ -20,6 +20,7 @@ commands = [testenv:py35] deps = flake8 + evdev coverage == 5.5 distro == 1.0.1 Jinja2 == 2.8 @@ -42,6 +43,7 @@ setenv= [testenv:py36] deps = flake8 + evdev coverage == 5.5 distro == 1.0.1 Jinja2 == 2.10 @@ -60,6 +62,7 @@ deps = [testenv:py38] deps = flake8 + evdev coverage == 7.3.0 distro == 1.4.0 Jinja2 == 2.10.1 @@ -77,6 +80,7 @@ deps = [testenv:py310] deps = flake8 + evdev coverage == 7.3.0 distro == 1.7.0 Jinja2 == 3.0.3 @@ -95,6 +99,7 @@ deps = [testenv:py312] deps = flake8 + evdev coverage == 7.4.4 distro == 1.9.0 Jinja2 == 3.1.2 diff --git a/providers/base/units/graphics/packaging.pxu b/providers/base/units/graphics/packaging.pxu index fa2a3ab3b3..d964e6168e 100644 --- a/providers/base/units/graphics/packaging.pxu +++ b/providers/base/units/graphics/packaging.pxu @@ -5,4 +5,4 @@ Depends: gnome-randr unit: packaging meta-data os-id: debian -Depends: gnome-screenshot +Depends: gnome-screenshot