Skip to content

Commit 52b934c

Browse files
GabrielChenCCstanley31huang
authored andcommitted
Add test case for randomly generated input events (New) (#1775)
Use python3-evdev to generate random input events (mouse movements, keyboard typing) in order to detect potential issues during screen rotation testing. Fix OEMQA-5102 Fix lp:2045249
1 parent 4f6c286 commit 52b934c

File tree

6 files changed

+329
-2
lines changed

6 files changed

+329
-2
lines changed

checkbox-ng/mk-venv

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ virtualenv --quiet --system-site-packages --python=python3 "$venv_path"
1515
# shellcheck source=/dev/null
1616
. "$venv_path"/bin/activate
1717
python3 -m pip install -e .
18-
pip install tqdm psutil
18+
pip install tqdm psutil evdev
1919

2020
mkdir -p "$venv_path/share/plainbox-providers-1"
2121
echo "export PROVIDERPATH=$venv_path/share/plainbox-providers-1" >> "$venv_path"/bin/activate

providers/base/bin/mouse_keyboard.py

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
#!/usr/bin/env python3
2+
#
3+
# This file is part of Checkbox.
4+
#
5+
# Copyright 2025 Canonical Ltd.
6+
#
7+
# Authors:
8+
# Gabriel Chen <[email protected]>
9+
#
10+
# Checkbox is free software: you can redistribute it and/or modify
11+
# it under the terms of the GNU General Public License version 3,
12+
# as published by the Free Software Foundation.
13+
#
14+
# Checkbox is distributed in the hope that it will be useful,
15+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
16+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17+
# GNU General Public License for more details.
18+
#
19+
# You should have received a copy of the GNU General Public License
20+
# along with Checkbox. If not, see <http://www.gnu.org/licenses/>.
21+
"""mouse_key_random utility."""
22+
23+
import time
24+
import random
25+
from evdev import UInput, ecodes as e
26+
27+
# Constants
28+
FREQUENCY_USEC = 100000 # Frequency of events in microseconds
29+
N_EPISODES = 81 # Number of events to generate
30+
WEIGHT_MOUSEMOVE = 10 # Weight of mouse movements
31+
WEIGHT_KEYPRESS = 1 # Weight of key presses
32+
WEIGHT_SUM = WEIGHT_MOUSEMOVE + WEIGHT_KEYPRESS # Total weight
33+
34+
MOVE_MAX = 100 # Maximum mouse movement distance
35+
MOVE_DELTA = 5 # Mouse movement step size
36+
37+
38+
# Define keyboard keys and mouse buttons
39+
KEYBOARD_KEYS = [
40+
e.KEY_A,
41+
e.KEY_B,
42+
e.KEY_C,
43+
e.KEY_D,
44+
e.KEY_E,
45+
e.KEY_F,
46+
e.KEY_G,
47+
e.KEY_H,
48+
e.KEY_I,
49+
e.KEY_J,
50+
e.KEY_K,
51+
e.KEY_L,
52+
e.KEY_M,
53+
e.KEY_N,
54+
e.KEY_O,
55+
e.KEY_P,
56+
e.KEY_Q,
57+
e.KEY_R,
58+
e.KEY_S,
59+
e.KEY_T,
60+
e.KEY_U,
61+
e.KEY_V,
62+
e.KEY_W,
63+
e.KEY_X,
64+
e.KEY_Y,
65+
e.KEY_Z,
66+
e.KEY_1,
67+
e.KEY_2,
68+
e.KEY_3,
69+
e.KEY_4,
70+
e.KEY_5,
71+
e.KEY_6,
72+
e.KEY_7,
73+
e.KEY_8,
74+
e.KEY_9,
75+
e.KEY_0,
76+
]
77+
MOUSE_BUTTONS = [e.BTN_LEFT, e.BTN_RIGHT]
78+
79+
80+
# Initialize the virtual input device
81+
def dev_init(name):
82+
# Define the capabilities of the device (keyboard and mouse events)
83+
capabilities = {
84+
e.EV_KEY: KEYBOARD_KEYS + MOUSE_BUTTONS,
85+
e.EV_REL: [e.REL_X, e.REL_Y],
86+
}
87+
# Create the virtual input device
88+
device = UInput(
89+
capabilities, name=name, vendor=0xBAD, product=0xA55, version=777
90+
)
91+
time.sleep(1) # Give userspace time to detect the new device
92+
return device
93+
94+
95+
# Destroy the virtual input device
96+
def dev_deinit(device):
97+
time.sleep(1) # Give userspace time to read the remaining events
98+
device.close()
99+
100+
101+
# Simulate a key press
102+
def key_press(device, key):
103+
device.write(e.EV_KEY, key, 1) # Press the key
104+
device.write(e.EV_KEY, key, 0) # Release the key
105+
device.syn() # Synchronize the event
106+
107+
108+
# Simulate mouse movement
109+
def mouse_move(device, x, y):
110+
device.write(e.EV_REL, e.REL_X, x) # Move mouse on the X axis
111+
device.write(e.EV_REL, e.REL_Y, y) # Move mouse on the Y axis
112+
device.syn() # Synchronize the event
113+
114+
115+
# Randomly press a key
116+
def rand_key_press(device):
117+
key = random.choice(KEYBOARD_KEYS) # Choose a random key
118+
key_press(device, key) # Simulate the key press
119+
time.sleep(FREQUENCY_USEC / 1000000.0) # Wait for the defined frequency
120+
121+
122+
# Randomly move the mouse
123+
def rand_mouse_moves(device):
124+
# Generate random X and Y movements
125+
x = random.randint(-MOVE_MAX // 2, MOVE_MAX // 2)
126+
y = random.randint(-MOVE_MAX // 2, MOVE_MAX // 2)
127+
steps = (
128+
max(abs(x), abs(y)) // MOVE_DELTA + 1
129+
) # Calculate the number of steps
130+
# Move the mouse in small steps for smooth movement
131+
for _ in range(steps):
132+
mouse_move(device, x // steps, y // steps)
133+
time.sleep(FREQUENCY_USEC / 1000000.0 / MOVE_DELTA)
134+
135+
# Handle any remaining movement
136+
rest_x = x % steps
137+
rest_y = y % steps
138+
if rest_x or rest_y:
139+
mouse_move(device, rest_x, rest_y)
140+
time.sleep(FREQUENCY_USEC / 1000000.0 / MOVE_DELTA)
141+
142+
143+
def main():
144+
# Initialize the virtual input device
145+
device = dev_init("umad")
146+
random.seed(time.time()) # Seed the random number generator
147+
148+
# Generate random events
149+
for _ in range(N_EPISODES):
150+
action = random.randint(0, WEIGHT_SUM - 1) # Choose a random action
151+
if action < WEIGHT_MOUSEMOVE:
152+
rand_mouse_moves(device) # Simulate mouse movement
153+
else:
154+
rand_key_press(device) # Simulate key press
155+
156+
# Destroy the virtual input device
157+
dev_deinit(device)
158+
159+
160+
if __name__ == "__main__":
161+
main()

providers/base/debian/control

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ Recommends: bonnie++,
3737
pm-utils,
3838
python3-apt,
3939
python3-dbus,
40+
python3-evdev,
4041
python3-gi,
4142
smartmontools,
4243
sysstat,
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
#!/usr/bin/env python3
2+
import unittest
3+
from unittest.mock import MagicMock, patch
4+
import time
5+
import random
6+
import evdev
7+
from evdev import ecodes as e
8+
from mouse_keyboard import (
9+
dev_init,
10+
dev_deinit,
11+
key_press,
12+
mouse_move,
13+
rand_key_press,
14+
rand_mouse_moves,
15+
main,
16+
KEYBOARD_KEYS,
17+
MOUSE_BUTTONS,
18+
FREQUENCY_USEC,
19+
MOVE_MAX,
20+
MOVE_DELTA,
21+
WEIGHT_MOUSEMOVE,
22+
WEIGHT_KEYPRESS,
23+
WEIGHT_SUM,
24+
N_EPISODES,
25+
)
26+
27+
28+
class TestMouseKeyboard(unittest.TestCase):
29+
30+
@patch("mouse_keyboard.UInput")
31+
@patch("mouse_keyboard.time.sleep")
32+
def test_dev_init(self, mock_sleep, mock_uinput):
33+
# Mock the UInput class
34+
mock_device = MagicMock()
35+
mock_uinput.return_value = mock_device
36+
37+
# Call the function
38+
device = dev_init("umad")
39+
40+
# Assertions
41+
mock_uinput.assert_called_once_with(
42+
{
43+
e.EV_KEY: KEYBOARD_KEYS + MOUSE_BUTTONS,
44+
e.EV_REL: [e.REL_X, e.REL_Y],
45+
},
46+
name="umad",
47+
vendor=0xBAD,
48+
product=0xA55,
49+
version=777,
50+
)
51+
self.assertEqual(device, mock_device)
52+
mock_sleep.assert_called_once_with(1)
53+
54+
@patch("mouse_keyboard.time.sleep")
55+
def test_dev_deinit(self, mock_sleep):
56+
# Mock the device
57+
mock_device = MagicMock()
58+
59+
# Call the function
60+
dev_deinit(mock_device)
61+
62+
# Assertions
63+
mock_sleep.assert_called_once_with(1)
64+
mock_device.close.assert_called_once_with()
65+
66+
def test_key_press(self):
67+
# Mock the device
68+
mock_device = MagicMock()
69+
70+
# Call the function
71+
key_press(mock_device, e.KEY_A)
72+
73+
# Assertions
74+
mock_device.write.assert_any_call(e.EV_KEY, e.KEY_A, 1)
75+
mock_device.write.assert_any_call(e.EV_KEY, e.KEY_A, 0)
76+
self.assertEqual(mock_device.syn.call_count, 1)
77+
78+
def test_mouse_move(self):
79+
# Mock the device
80+
mock_device = MagicMock()
81+
82+
# Call the function
83+
mouse_move(mock_device, 10, 20)
84+
85+
# Assertions
86+
mock_device.write.assert_any_call(e.EV_REL, e.REL_X, 10)
87+
mock_device.write.assert_any_call(e.EV_REL, e.REL_Y, 20)
88+
mock_device.syn.assert_called_once_with()
89+
90+
@patch("mouse_keyboard.random.choice")
91+
@patch("mouse_keyboard.time.sleep")
92+
def test_rand_key_press(self, mock_sleep, mock_choice):
93+
# Mock the device and random choice
94+
mock_device = MagicMock()
95+
mock_choice.return_value = e.KEY_B
96+
97+
# Call the function
98+
rand_key_press(mock_device)
99+
100+
# Assertions
101+
mock_choice.assert_called_once_with(KEYBOARD_KEYS)
102+
mock_device.write.assert_any_call(e.EV_KEY, e.KEY_B, 1)
103+
mock_device.write.assert_any_call(e.EV_KEY, e.KEY_B, 0)
104+
mock_sleep.assert_called_once_with(FREQUENCY_USEC / 1000000.0)
105+
106+
@patch("mouse_keyboard.random.randint")
107+
@patch("mouse_keyboard.time.sleep")
108+
def test_rand_mouse_moves(self, mock_sleep, mock_randint):
109+
# Mock the device and random.randint
110+
mock_device = MagicMock()
111+
mock_randint.side_effect = [50, -30] # x, y
112+
113+
# Call the function
114+
rand_mouse_moves(mock_device)
115+
116+
# Assertions
117+
self.assertEqual(mock_randint.call_count, 2)
118+
mock_device.write.assert_any_call(
119+
e.EV_REL, e.REL_X, 50 // (50 // MOVE_DELTA + 1)
120+
)
121+
mock_device.write.assert_any_call(
122+
e.EV_REL, e.REL_Y, -30 // (50 // MOVE_DELTA + 1)
123+
)
124+
self.assertGreaterEqual(mock_sleep.call_count, 1)
125+
126+
@patch("mouse_keyboard.time.time")
127+
@patch("mouse_keyboard.rand_mouse_moves")
128+
@patch("mouse_keyboard.random.seed")
129+
@patch("mouse_keyboard.random.randint")
130+
@patch("mouse_keyboard.dev_init")
131+
@patch("mouse_keyboard.dev_deinit")
132+
def test_main(
133+
self,
134+
mock_dev_deinit,
135+
mock_dev_init,
136+
mock_randint,
137+
mock_seed,
138+
mock_rand_mouse_moves,
139+
mock_time,
140+
):
141+
# Mock the device and random functions
142+
mock_device = MagicMock()
143+
mock_dev_init.return_value = mock_device
144+
mock_randint.side_effect = [
145+
0,
146+
10,
147+
] * N_EPISODES # Always choose mouse movement
148+
149+
# Call the function
150+
main()
151+
152+
# Assertions
153+
mock_seed.assert_called_once_with(mock_time())
154+
mock_dev_init.assert_called_once_with("umad")
155+
self.assertEqual(mock_randint.call_count, N_EPISODES)
156+
mock_dev_deinit.assert_called_once_with(mock_device)
157+
158+
159+
if __name__ == "__main__":
160+
unittest.main()

providers/base/tox.ini

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ setenv = PROVIDERPATH = {envdir}
1818
[testenv:py35]
1919
deps =
2020
flake8
21+
evdev
2122
coverage == 5.5
2223
distro == 1.0.1
2324
Jinja2 == 2.8
@@ -41,6 +42,7 @@ setenv=
4142
[testenv:py36]
4243
deps =
4344
flake8
45+
evdev
4446
coverage == 5.5
4547
distro == 1.0.1
4648
Jinja2 == 2.10
@@ -59,6 +61,7 @@ deps =
5961
[testenv:py38]
6062
deps =
6163
flake8
64+
evdev
6265
coverage == 7.3.0
6366
distro == 1.4.0
6467
Jinja2 == 2.10.1
@@ -76,6 +79,7 @@ deps =
7679
[testenv:py310]
7780
deps =
7881
flake8
82+
evdev
7983
coverage == 7.3.0
8084
distro == 1.7.0
8185
Jinja2 == 3.0.3
@@ -94,6 +98,7 @@ deps =
9498
[testenv:py312]
9599
deps =
96100
flake8
101+
evdev
97102
coverage == 7.4.4
98103
distro == 1.9.0
99104
Jinja2 == 3.1.2

providers/base/units/graphics/packaging.pxu

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,4 +5,4 @@ Depends: gnome-randr
55

66
unit: packaging meta-data
77
os-id: debian
8-
Depends: gnome-screenshot
8+
Depends: gnome-screenshot

0 commit comments

Comments
 (0)