Skip to content

Commit 311b550

Browse files
authored
Add webgl conformance test (New) (#2115)
1. add file_watcher in checkbox support 2. add WebGL conformance test script 3. add test cases and test plan 4. add readme
1 parent 6adbd50 commit 311b550

File tree

9 files changed

+1024
-0
lines changed

9 files changed

+1024
-0
lines changed
Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
#!/usr/bin/env python3
2+
# Copyright 2025 Canonical Ltd.
3+
# All rights reserved.
4+
#
5+
# Written by:
6+
# Hanhsuan Lee <hanhsuan.lee@canonical.com>
7+
#
8+
# Checkbox is free software: you can redistribute it and/or modify
9+
# it under the terms of the GNU General Public License version 3,
10+
# as published by the Free Software Foundation.
11+
#
12+
# Checkbox is distributed in the hope that it will be useful,
13+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
14+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15+
# GNU General Public License for more details.
16+
#
17+
# You should have received a copy of the GNU General Public License
18+
# along with Checkbox. If not, see <http://www.gnu.org/licenses/>.
19+
"""
20+
checkbox_support.helpers.file_watcher import FileWatcher
21+
=============================================
22+
23+
Utility class for watching file create/modify/delete event with inotify
24+
"""
25+
import ctypes
26+
import struct
27+
import os
28+
29+
30+
class InotifyEvent:
31+
"""
32+
A class to store inotify system event
33+
"""
34+
35+
def __init__(self, wd: int, event_type: str, cookie: int, name: str):
36+
self.wd = wd # Watch descriptor
37+
self.event_type = event_type # modify, create, delete or unknown
38+
self.cookie = (
39+
cookie # unique cookie associating related events (for rename)
40+
)
41+
self.name = name # file name. might be empty string
42+
43+
def __str__(self):
44+
return "InotifyEvent(wd={}, event_type={}, cookie={}, name={})".format(
45+
self.wd, self.event_type, self.cookie, self.name
46+
)
47+
48+
49+
class FileWatcher:
50+
"""
51+
This class helps to use inotify system event to monitor file status
52+
"""
53+
54+
libc = ctypes.CDLL("libc.so.6")
55+
56+
# inotify constants
57+
_IN_MODIFY = 0x00000002
58+
_IN_CREATE = 0x00000100
59+
_IN_DELETE = 0x00000200
60+
61+
# The Linux inotify_event struct minus the flexible name array
62+
# Watch descriptor: int
63+
# Mask of events: uint32_t
64+
# Unique cookie associating related events (for rename): uint32_t
65+
# Size of name field: uint32_t
66+
# Optional null-terminated name: char
67+
_EVENT_STRUCT_FORMAT = "iIII"
68+
_EVENT_STRUCT_SIZE = struct.calcsize(_EVENT_STRUCT_FORMAT)
69+
70+
def __init__(self):
71+
self.fd = self.libc.inotify_init()
72+
if self.fd < 0:
73+
raise SystemExit("Failed to initialize inotify")
74+
75+
def watch_directory(self, path: str, event: str) -> int:
76+
"""
77+
Set the watching path and event
78+
79+
:param path:
80+
The full path of directory
81+
82+
:param event:
83+
c: file create event
84+
m: file modify event
85+
d: file delete event
86+
Could combine them in any order, such as "mc", "dmc"
87+
:returns:
88+
Watch descriptor
89+
"""
90+
mask = 0x00000000
91+
if "m" in event:
92+
mask = mask | self._IN_MODIFY
93+
if "d" in event:
94+
mask = mask | self._IN_DELETE
95+
if "c" in event:
96+
mask = mask | self._IN_CREATE
97+
if mask == 0x00000000:
98+
mask = self._IN_MODIFY
99+
return self.libc.inotify_add_watch(self.fd, path.encode("utf-8"), mask)
100+
101+
def stop_watch(self, wd: int):
102+
"""
103+
Remove the watching path and event
104+
105+
:param wd:
106+
Watch descriptor
107+
"""
108+
return self.libc.inotify_rm_watch(self.fd, wd)
109+
110+
def _mask2event(self, mask: int) -> str:
111+
"""
112+
Convert mask to human readable message
113+
114+
:param mask:
115+
inotify event mask
116+
:returns:
117+
modify, create, delete or unknown
118+
"""
119+
event_map = {
120+
self._IN_MODIFY: "modify",
121+
self._IN_CREATE: "create",
122+
self._IN_DELETE: "delete",
123+
}
124+
return event_map.get(mask, "unknown")
125+
126+
def read_events(self, size: int) -> list:
127+
"""
128+
Start reading event
129+
130+
:param size:
131+
event reading size
132+
:returns:
133+
parsed event information list
134+
"""
135+
try:
136+
raw_data = os.read(self.fd, 1024 if size < 0 else size)
137+
event = []
138+
while len(raw_data) > 0:
139+
wd, mask, cookie, length = struct.unpack(
140+
self._EVENT_STRUCT_FORMAT,
141+
raw_data[: self._EVENT_STRUCT_SIZE],
142+
)
143+
raw_data = raw_data[self._EVENT_STRUCT_SIZE :]
144+
name = (
145+
raw_data[:length].rstrip(b"\0").decode("utf-8")
146+
if length > 0
147+
else ""
148+
)
149+
event.append(
150+
InotifyEvent(wd, self._mask2event(mask), cookie, name)
151+
)
152+
raw_data = raw_data[length:]
153+
return event
154+
except KeyboardInterrupt:
155+
self.stop_watch(self.fd)
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
from checkbox_support.helpers.file_watcher import FileWatcher, InotifyEvent
2+
from unittest.mock import MagicMock, patch
3+
import unittest
4+
5+
6+
class TestInotifyEvent(unittest.TestCase):
7+
"""
8+
Test the InotifyEvent data class.
9+
"""
10+
11+
def test_inotify_event_creation(self):
12+
event = InotifyEvent(
13+
wd=1, event_type="modify", cookie=0, name="test_file.txt"
14+
)
15+
self.assertEqual(event.wd, 1)
16+
self.assertEqual(event.event_type, "modify")
17+
self.assertEqual(event.cookie, 0)
18+
self.assertEqual(event.name, "test_file.txt")
19+
20+
21+
class TestFileWatcher(unittest.TestCase):
22+
"""
23+
Test the FileWatcher class by mocking system-level calls.
24+
"""
25+
26+
@patch.object(FileWatcher, "libc")
27+
def test_initialization_success(self, mock_libc):
28+
mock_libc.inotify_init.return_value = 123
29+
watcher = FileWatcher()
30+
self.assertEqual(watcher.fd, 123)
31+
32+
@patch.object(FileWatcher, "libc")
33+
def test_initialization_failure(self, mock_libc):
34+
mock_libc.inotify_init.return_value = -1
35+
with self.assertRaises(SystemExit) as cm:
36+
FileWatcher()
37+
self.assertEqual(cm.exception.code, "Failed to initialize inotify")
38+
39+
@patch.object(FileWatcher, "libc")
40+
def test_watch_directory_with_single_event(self, mock_libc):
41+
mock_libc.inotify_init.return_value = 123
42+
watcher = FileWatcher()
43+
44+
test_path = "/tmp/test_dir"
45+
mock_watch_desc = 456
46+
mock_libc.inotify_add_watch.return_value = mock_watch_desc
47+
48+
wd = watcher.watch_directory(test_path, "m")
49+
mock_libc.inotify_add_watch.assert_called_with(
50+
123, test_path.encode("utf-8"), 0x00000002
51+
)
52+
self.assertEqual(wd, mock_watch_desc)
53+
54+
@patch.object(FileWatcher, "libc")
55+
def test_watch_directory_with_multiple_events(self, mock_libc):
56+
mock_libc.inotify_init.return_value = 123
57+
watcher = FileWatcher()
58+
59+
test_path = "/tmp/test_dir"
60+
mock_watch_desc = 789
61+
mock_libc.inotify_add_watch.return_value = mock_watch_desc
62+
63+
wd = watcher.watch_directory(test_path, "dmc")
64+
expected_mask = 0x00000200 | 0x00000002 | 0x00000100
65+
mock_libc.inotify_add_watch.assert_called_with(
66+
123, test_path.encode("utf-8"), expected_mask
67+
)
68+
self.assertEqual(wd, mock_watch_desc)
69+
70+
@patch.object(FileWatcher, "libc")
71+
def test_stop_watch(self, mock_libc):
72+
mock_libc.inotify_init.return_value = 123
73+
watcher = FileWatcher()
74+
75+
mock_watch_desc = 1011
76+
watcher.stop_watch(mock_watch_desc)
77+
mock_libc.inotify_rm_watch.assert_called_with(123, mock_watch_desc)
78+
79+
def test_mask2event(self):
80+
watcher = FileWatcher()
81+
self.assertEqual(watcher._mask2event(0x00000002), "modify")
82+
self.assertEqual(watcher._mask2event(0x00000100), "create")
83+
self.assertEqual(watcher._mask2event(0x00000200), "delete")
84+
self.assertEqual(watcher._mask2event(0x0000FFFF), "unknown")
85+
86+
@patch("checkbox_support.helpers.file_watcher.os.read")
87+
@patch("checkbox_support.helpers.file_watcher.InotifyEvent")
88+
@patch.object(FileWatcher, "libc")
89+
def test_read_events_single_event(
90+
self, mock_libc, mock_inotify_event, mock_os_read
91+
):
92+
mock_libc.inotify_init.return_value = 123
93+
watcher = FileWatcher()
94+
95+
mock_os_read.return_value = b"\x01\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x09\x00\x00\x00filename\x00"
96+
97+
mock_event_instance = MagicMock()
98+
mock_inotify_event.return_value = mock_event_instance
99+
100+
events = watcher.read_events(1024)
101+
102+
mock_os_read.assert_called_with(watcher.fd, 1024)
103+
104+
self.assertEqual(len(events), 1)
105+
106+
mock_inotify_event.assert_called_with(1, "modify", 0, "filename")
107+
108+
self.assertEqual(events[0], mock_event_instance)
109+
110+
@patch("checkbox_support.helpers.file_watcher.os.read")
111+
@patch("checkbox_support.helpers.file_watcher.InotifyEvent")
112+
@patch.object(FileWatcher, "libc")
113+
def test_read_events_multiple_events(
114+
self, mock_libc, mock_inotify_event, mock_os_read
115+
):
116+
# Mock multiple events in a single read
117+
mock_libc.inotify_init.return_value = 123
118+
watcher = FileWatcher()
119+
120+
# Mock data for two events
121+
mock_os_read.return_value = (
122+
b"\x01\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00\x09\x00\x00\x00file_one\x00"
123+
+ b"\x02\x00\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x09\x00\x00\x00file_two\x00"
124+
)
125+
126+
events = watcher.read_events(1024)
127+
128+
self.assertEqual(len(events), 2)
129+
mock_inotify_event.assert_any_call(1, "modify", 0, "file_one")
130+
mock_inotify_event.assert_any_call(2, "delete", 0, "file_two")
131+
132+
@patch(
133+
"checkbox_support.helpers.file_watcher.os.read",
134+
side_effect=KeyboardInterrupt,
135+
)
136+
@patch.object(FileWatcher, "stop_watch")
137+
@patch.object(FileWatcher, "libc")
138+
def test_read_events_keyboard_interrupt(
139+
self, mock_libc, mock_stop_watch, mock_os_read
140+
):
141+
mock_libc.inotify_init.return_value = 123
142+
watcher = FileWatcher()
143+
144+
watcher.read_events(1024)
145+
mock_stop_watch.assert_called_with(watcher.fd)
146+
147+
148+
if __name__ == "__main__":
149+
unittest.main()

0 commit comments

Comments
 (0)