xref: /linux/tools/testing/selftests/devices/probe/test_discoverable_devices.py (revision be602cde657ee43d23adbf309be6d700d0106dc9)
1#!/usr/bin/python3
2# SPDX-License-Identifier: GPL-2.0
3#
4# Copyright (c) 2023 Collabora Ltd
5#
6# This script tests for presence and driver binding of devices from discoverable
7# buses (ie USB, PCI).
8#
9# The per-platform YAML file defining the devices to be tested is stored inside
10# the boards/ directory and chosen based on DT compatible or DMI IDs (sys_vendor
11# and product_name).
12#
13# See boards/google,spherion.yaml and boards/'Dell Inc.,XPS 13 9300.yaml' for
14# the description and examples of the file structure and vocabulary.
15#
16
17import argparse
18import glob
19import os
20import re
21import sys
22import yaml
23
24# Allow ksft module to be imported from different directory
25this_dir = os.path.dirname(os.path.realpath(__file__))
26sys.path.append(os.path.join(this_dir, "../../kselftest/"))
27
28import ksft
29
30pci_controllers = []
31usb_controllers = []
32
33sysfs_usb_devices = "/sys/bus/usb/devices/"
34
35
36def find_pci_controller_dirs():
37    sysfs_devices = "/sys/devices"
38    pci_controller_sysfs_dir = "pci[0-9a-f]{4}:[0-9a-f]{2}"
39
40    dir_regex = re.compile(pci_controller_sysfs_dir)
41    for path, dirs, _ in os.walk(sysfs_devices):
42        for d in dirs:
43            if dir_regex.match(d):
44                pci_controllers.append(os.path.join(path, d))
45
46
47def find_usb_controller_dirs():
48    usb_controller_sysfs_dir = r"usb[\d]+"
49
50    dir_regex = re.compile(usb_controller_sysfs_dir)
51    for d in os.scandir(sysfs_usb_devices):
52        if dir_regex.match(d.name):
53            usb_controllers.append(os.path.realpath(d.path))
54
55
56def get_dt_mmio(sysfs_dev_dir):
57    re_dt_mmio = re.compile("OF_FULLNAME=.*@([0-9a-f]+)")
58    dt_mmio = None
59
60    # PCI controllers' sysfs don't have an of_node, so have to read it from the
61    # parent
62    while not dt_mmio:
63        try:
64            with open(os.path.join(sysfs_dev_dir, "uevent")) as f:
65                dt_mmio = re_dt_mmio.search(f.read()).group(1)
66                return dt_mmio
67        except:
68            pass
69        sysfs_dev_dir = os.path.dirname(sysfs_dev_dir)
70
71
72def get_of_fullname(sysfs_dev_dir):
73    re_of_fullname = re.compile("OF_FULLNAME=(.*)")
74    of_full_name = None
75
76    # PCI controllers' sysfs don't have an of_node, so have to read it from the
77    # parent
78    while not of_full_name:
79        try:
80            with open(os.path.join(sysfs_dev_dir, "uevent")) as f:
81                of_fullname = re_of_fullname.search(f.read()).group(1)
82                return of_fullname
83        except:
84            pass
85        sysfs_dev_dir = os.path.dirname(sysfs_dev_dir)
86
87
88def get_acpi_uid(sysfs_dev_dir):
89    with open(os.path.join(sysfs_dev_dir, "firmware_node", "uid")) as f:
90        return f.read()
91
92
93def get_usb_version(sysfs_dev_dir):
94    re_usb_version = re.compile(r"PRODUCT=.*/(\d)/.*")
95    with open(os.path.join(sysfs_dev_dir, "uevent")) as f:
96        return int(re_usb_version.search(f.read()).group(1))
97
98
99def get_usb_busnum(sysfs_dev_dir):
100    re_busnum = re.compile("BUSNUM=(.*)")
101    with open(os.path.join(sysfs_dev_dir, "uevent")) as f:
102        return int(re_busnum.search(f.read()).group(1))
103
104
105def find_controller_in_sysfs(controller, parent_sysfs=None):
106    if controller["type"] == "pci-controller":
107        controllers = pci_controllers
108    elif controller["type"] == "usb-controller":
109        controllers = usb_controllers
110
111    result_controllers = []
112
113    for c in controllers:
114        if parent_sysfs and parent_sysfs not in c:
115            continue
116
117        if controller.get("dt-mmio"):
118            if str(controller["dt-mmio"]) != get_dt_mmio(c):
119                continue
120
121        if controller.get("of-fullname-regex"):
122            re_of_fullname = re.compile(str(controller["of-fullname-regex"]))
123            if not re_of_fullname.match(get_of_fullname(c)):
124                continue
125
126        if controller.get("usb-version"):
127            if controller["usb-version"] != get_usb_version(c):
128                continue
129
130        if controller.get("acpi-uid"):
131            if controller["acpi-uid"] != get_acpi_uid(c):
132                continue
133
134        result_controllers.append(c)
135
136    return result_controllers
137
138
139def is_controller(device):
140    return device.get("type") and "controller" in device.get("type")
141
142
143def path_to_dir(parent_sysfs, dev_type, path):
144    if dev_type == "usb-device":
145        usb_dev_sysfs_fmt = "{}-{}"
146        busnum = get_usb_busnum(parent_sysfs)
147        dirname = os.path.join(
148            sysfs_usb_devices, usb_dev_sysfs_fmt.format(busnum, path)
149        )
150        return [os.path.realpath(dirname)]
151    else:
152        pci_dev_sysfs_fmt = "????:??:{}"
153        path_glob = ""
154        for dev_func in path.split("/"):
155            dev_func = dev_func.zfill(4)
156            path_glob = os.path.join(path_glob, pci_dev_sysfs_fmt.format(dev_func))
157
158        dir_list = glob.glob(os.path.join(parent_sysfs, path_glob))
159
160        return dir_list
161
162
163def find_in_sysfs(device, parent_sysfs=None):
164    if parent_sysfs and device.get("path"):
165        pathdirs = path_to_dir(
166            parent_sysfs, device["meta"]["type"], str(device["path"])
167        )
168        if len(pathdirs) != 1:
169            # Early return to report error
170            return pathdirs
171        pathdir = pathdirs[0]
172        sysfs_path = os.path.join(parent_sysfs, pathdir)
173    else:
174        sysfs_path = parent_sysfs
175
176    if is_controller(device):
177        return find_controller_in_sysfs(device, sysfs_path)
178    else:
179        return [sysfs_path]
180
181
182def check_driver_presence(sysfs_dir, current_node):
183    if current_node["meta"]["type"] == "usb-device":
184        usb_intf_fmt = "*-*:*.{}"
185
186        interfaces = []
187        for i in current_node["interfaces"]:
188            interfaces.append((i, usb_intf_fmt.format(i)))
189
190        for intf_num, intf_dir_fmt in interfaces:
191            test_name = f"{current_node['meta']['pathname']}.{intf_num}.driver"
192
193            intf_dirs = glob.glob(os.path.join(sysfs_dir, intf_dir_fmt))
194            if len(intf_dirs) != 1:
195                ksft.test_result_fail(test_name)
196                continue
197            intf_dir = intf_dirs[0]
198
199            driver_link = os.path.join(sysfs_dir, intf_dir, "driver")
200            ksft.test_result(os.path.isdir(driver_link), test_name)
201    else:
202        driver_link = os.path.join(sysfs_dir, "driver")
203        test_name = current_node["meta"]["pathname"] + ".driver"
204        ksft.test_result(os.path.isdir(driver_link), test_name)
205
206
207def generate_pathname(device):
208    pathname = ""
209
210    if device.get("path"):
211        pathname = str(device["path"])
212
213    if device.get("type"):
214        dev_type = device["type"]
215        if device.get("usb-version"):
216            dev_type = dev_type.replace("usb", "usb" + str(device["usb-version"]))
217        if device.get("acpi-uid") is not None:
218            dev_type = dev_type.replace("pci", "pci" + str(device["acpi-uid"]))
219        pathname = pathname + "/" + dev_type
220
221    if device.get("dt-mmio"):
222        pathname += "@" + str(device["dt-mmio"])
223
224    if device.get("of-fullname-regex"):
225        pathname += "-" + str(device["of-fullname-regex"])
226
227    if device.get("name"):
228        pathname = pathname + "/" + device["name"]
229
230    return pathname
231
232
233def fill_meta_keys(child, parent=None):
234    child["meta"] = {}
235
236    if parent:
237        child["meta"]["type"] = parent["type"].replace("controller", "device")
238
239    pathname = generate_pathname(child)
240    if parent:
241        pathname = parent["meta"]["pathname"] + "/" + pathname
242    child["meta"]["pathname"] = pathname
243
244
245def parse_device_tree_node(current_node, parent_sysfs=None):
246    if not parent_sysfs:
247        fill_meta_keys(current_node)
248
249    sysfs_dirs = find_in_sysfs(current_node, parent_sysfs)
250    if len(sysfs_dirs) != 1:
251        if len(sysfs_dirs) == 0:
252            ksft.test_result_fail(
253                f"Couldn't find in sysfs: {current_node['meta']['pathname']}"
254            )
255        else:
256            ksft.test_result_fail(
257                f"Found multiple sysfs entries for {current_node['meta']['pathname']}: {sysfs_dirs}"
258            )
259        return
260    sysfs_dir = sysfs_dirs[0]
261
262    if not is_controller(current_node):
263        ksft.test_result(
264            os.path.exists(sysfs_dir), current_node["meta"]["pathname"] + ".device"
265        )
266        check_driver_presence(sysfs_dir, current_node)
267    else:
268        for child_device in current_node["devices"]:
269            fill_meta_keys(child_device, current_node)
270            parse_device_tree_node(child_device, sysfs_dir)
271
272
273def count_tests(device_trees):
274    test_count = 0
275
276    def parse_node(device):
277        nonlocal test_count
278        if device.get("devices"):
279            for child in device["devices"]:
280                parse_node(child)
281        else:
282            if device.get("interfaces"):
283                test_count += len(device["interfaces"])
284            else:
285                test_count += 1
286            test_count += 1
287
288    for device_tree in device_trees:
289        parse_node(device_tree)
290
291    return test_count
292
293
294def get_board_filenames():
295    filenames = []
296
297    platform_compatible_file = "/proc/device-tree/compatible"
298    if os.path.exists(platform_compatible_file):
299        with open(platform_compatible_file) as f:
300            for line in f:
301                filenames.extend(line.split("\0"))
302    else:
303        dmi_id_dir = "/sys/devices/virtual/dmi/id"
304        vendor_dmi_file = os.path.join(dmi_id_dir, "sys_vendor")
305        product_dmi_file = os.path.join(dmi_id_dir, "product_name")
306
307        with open(vendor_dmi_file) as f:
308            vendor = f.read().replace("\n", "")
309        with open(product_dmi_file) as f:
310            product = f.read().replace("\n", "")
311
312        filenames = [vendor + "," + product]
313
314    return filenames
315
316
317def run_test(yaml_file):
318    ksft.print_msg(f"Using board file: {yaml_file}")
319
320    with open(yaml_file) as f:
321        device_trees = yaml.safe_load(f)
322
323    ksft.set_plan(count_tests(device_trees))
324
325    for device_tree in device_trees:
326        parse_device_tree_node(device_tree)
327
328
329parser = argparse.ArgumentParser()
330parser.add_argument(
331    "--boards-dir", default="boards", help="Directory containing the board YAML files"
332)
333args = parser.parse_args()
334
335find_pci_controller_dirs()
336find_usb_controller_dirs()
337
338ksft.print_header()
339
340if not os.path.exists(args.boards_dir):
341    ksft.print_msg(f"Boards directory '{args.boards_dir}' doesn't exist")
342    ksft.exit_fail()
343
344board_file = ""
345for board_filename in get_board_filenames():
346    full_board_filename = os.path.join(args.boards_dir, board_filename + ".yaml")
347
348    if os.path.exists(full_board_filename):
349        board_file = full_board_filename
350        break
351
352if not board_file:
353    ksft.print_msg("No matching board file found")
354    ksft.exit_fail()
355
356run_test(board_file)
357
358ksft.finished()
359