From d7d93ff57fa012f035237f570880ea4989d1525d Mon Sep 17 00:00:00 2001 From: Mahasvan Date: Sat, 1 Aug 2026 15:06:56 +0530 Subject: [PATCH 01/10] Improve code quality, fix minor bugs --- src/hwprobe/core/common/edid.py | 5 +- src/hwprobe/core/linux/cpu.py | 2 +- src/hwprobe/core/linux/graphics.py | 4 +- src/hwprobe/core/mac/cpu.py | 6 +- src/hwprobe/core/mac/deprecated/common.py | 66 ------- src/hwprobe/core/mac/deprecated/ioreg.py | 199 ---------------------- src/hwprobe/core/mac/display.py | 3 +- src/hwprobe/core/mac/graphics.py | 102 ----------- src/hwprobe/core/mac/memory.py | 23 ++- src/hwprobe/core/mac/network.py | 18 +- src/hwprobe/core/windows/baseboard.py | 2 +- src/hwprobe/core/windows/storage.py | 2 - src/hwprobe/util/location_paths.py | 22 +-- 13 files changed, 42 insertions(+), 412 deletions(-) delete mode 100644 src/hwprobe/core/mac/deprecated/common.py delete mode 100644 src/hwprobe/core/mac/deprecated/ioreg.py diff --git a/src/hwprobe/core/common/edid.py b/src/hwprobe/core/common/edid.py index 7960662..b760931 100644 --- a/src/hwprobe/core/common/edid.py +++ b/src/hwprobe/core/common/edid.py @@ -83,15 +83,14 @@ def parse_edid(edid_data: bytes) -> DisplayModuleInfo: for block_start in range(0x36, 0x6d, 18): block = edid_data[block_start:block_start + 18] - zeros = 0x00.to_bytes(1, byteorder='little') * 2 - if block[:2] == zeros: + if block[:2] == b"\x00\x00": tag = block[3] if tag in DESCRIPTOR_TAG_ENUM: # Refer to DESCRIPTOR_TAG_ENUM for valid block type codes if tag == 0xFF: # todo: test if this works module.serial_number = block[5:].decode("ascii").strip() - if tag == 0xFC: + elif tag == 0xFC: module.name = block[5:].decode("ascii").strip() else: diff --git a/src/hwprobe/core/linux/cpu.py b/src/hwprobe/core/linux/cpu.py index b1005a9..9e9da50 100644 --- a/src/hwprobe/core/linux/cpu.py +++ b/src/hwprobe/core/linux/cpu.py @@ -55,7 +55,7 @@ def _cpu_threads(raw_cpu_info: str) -> Optional[int]: try: count = len(re.findall(r"^processor\s+:", raw_cpu_info, re.MULTILINE)) return count if count > 0 else None - except: + except Exception: return None diff --git a/src/hwprobe/core/linux/graphics.py b/src/hwprobe/core/linux/graphics.py index 98e8766..a3e0e54 100644 --- a/src/hwprobe/core/linux/graphics.py +++ b/src/hwprobe/core/linux/graphics.py @@ -27,7 +27,7 @@ def _vram_amd(device) -> Optional[int]: vram_mb = int(vram_bits / 1024 / 1024) return vram_mb return None - except: + except Exception: return None @@ -101,7 +101,7 @@ def _populate_lspci_info(gpu: GPUInfo, device: str) -> GPUInfo: # We gather all data here and parse whatever data we have. Subsystem data may not be returned. except Exception as e: # lspci may not be available in some distros - raise e + raise data = {} for line in lspci_output.splitlines(): diff --git a/src/hwprobe/core/mac/cpu.py b/src/hwprobe/core/mac/cpu.py index b796e55..21f4f0e 100644 --- a/src/hwprobe/core/mac/cpu.py +++ b/src/hwprobe/core/mac/cpu.py @@ -53,7 +53,7 @@ def fetch_cpu_info() -> CPUInfo: try: bitness_64 = subprocess.check_output(["sysctl", "hw.cpu64bit_capable"]).decode() - bitness_64 = True if bitness_64.split(": ")[1].strip() == "1" else False + bitness_64 = bitness_64.split(": ")[1].strip() == "1" if bitness_64: cpu_info.bitness = 64 @@ -125,10 +125,10 @@ def fetch_cpu_info() -> CPUInfo: if "arm" in arch.lower(): try: sme_presence = subprocess.check_output(["sysctl", "hw.optional.arm.FEAT_SME"]).decode() - sme_presence = True if sme_presence.split(": ")[1].strip() == "1" else False + sme_presence = sme_presence.split(": ")[1].strip() == "1" sme2_presence = subprocess.check_output(["sysctl", "hw.optional.arm.FEAT_SME2"]).decode() - sme2_presence = True if sme2_presence.split(": ")[1].strip() == "1" else False + sme2_presence = sme2_presence.split(": ")[1].strip() == "1" if sme_presence or sme2_presence: cpu_info.arch_version = "9" diff --git a/src/hwprobe/core/mac/deprecated/common.py b/src/hwprobe/core/mac/deprecated/common.py deleted file mode 100644 index 30c7c30..0000000 --- a/src/hwprobe/core/mac/deprecated/common.py +++ /dev/null @@ -1,66 +0,0 @@ -""" -Usage of these functions are deprecated. -This functionality has been moved to the C++ layer for better integration with IOKit. -Refer interops/mac for the rewritten implementation. -""" - -# Original source: -# https://github.com/dortania/OpenCore-Legacy-Patcher/blob/ca859c7ad7ac2225af3b50626d88f3bfe014eaa8/resources/device_probe.py#L67-L93 -# Copied from - https://github.com/KernelWanderers/OCSysInfo/blob/main/src/util/pci_root.py -from hwprobe.core.mac.deprecated.ioreg import * - - -def construct_pci_path_mac(parent_entry, acpi): - data = { - "pci_path": "", - "acpi_path": "" - } - paths = [] - entry = parent_entry - - while entry: - if IOObjectConformsTo(entry, b'IOPCIDevice'): - try: - bus, func = ([ - hex(int(i, 16)) for i in - ioname_t_to_str( - IORegistryEntryGetLocationInPlane( - entry, b'IOService', None - )[1] - ).split(',') - ] + ['0x0'])[:2] - - paths.append( - f'Pci({bus},{func})' - ) - except ValueError: - break - - elif IOObjectConformsTo(entry, b'IOACPIPlatformDevice'): - paths.append( - f'PciRoot({hex(int(corefoundation_to_native(IORegistryEntryCreateCFProperty(entry, "_UID", kCFAllocatorDefault, kNilOptions)) or 0))})') - break - - elif IOObjectConformsTo(entry, b'IOPCIBridge'): - pass - - else: - paths = [] - # Invalid PCI device – unable to construct PCI path - break - - parent = IORegistryEntryGetParentEntry(entry, b'IOService', None)[1] - - if entry != parent_entry: - IOObjectRelease(entry) - - entry = parent - - if paths: - data['pci_path'] = '/'.join(reversed(paths)) - - if acpi: - data['acpi_path'] = ''.join([("\\" if "sb" in a.lower( - ) else ".") + a.split("@")[0] for a in acpi.split(':')[1].split('/')[1:]]) - - return data diff --git a/src/hwprobe/core/mac/deprecated/ioreg.py b/src/hwprobe/core/mac/deprecated/ioreg.py deleted file mode 100644 index b5883c0..0000000 --- a/src/hwprobe/core/mac/deprecated/ioreg.py +++ /dev/null @@ -1,199 +0,0 @@ -""" -Usage of these functions are deprecated. -This functionality has been moved to the C++ layer for better integration with IOKit. -Refer interops/mac for the rewritten implementation. -""" - -# Credits to @[DhinakG](https://github.com/DhinkaG) for allowing us to copy over their `ioreg.py` abstraction implementation from OpenCore-Legacy-Patcher: -# https://github.com/dortania/OpenCore-Legacy-Patcher/blob/f6ef7583eedc706e2bb70550fe847601ef258fcd/resources/ioreg.py - -from typing import NewType, Union - -import objc -from CoreFoundation import CFRelease, kCFAllocatorDefault -from Foundation import NSBundle -from PyObjCTools import Conversion - -IOKit = NSBundle.bundleWithIdentifier_("com.apple.framework.IOKit") - -io_name_t_ref_out = b"[128c]" # io_name_t is char[128] -const_io_name_t_ref_in = b"r*" -CFStringRef = b"^{__CFString=}" -CFDictionaryRef = b"^{__CFDictionary=}" -CFAllocatorRef = b"^{__CFAllocator=}" - -# https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtTypeEncodings.html -functions = [ - ("IORegistryEntryCreateCFProperties", b"IIo^@" + CFAllocatorRef + b"I"), - ("IORegistryEntryGetChildIterator", b"IIr*o^I"), - ("IORegistryEntryGetLocationInPlane", b"II" + const_io_name_t_ref_in + b"o" + io_name_t_ref_out), - ("IORegistryEntryGetRegistryEntryID", b"IIo^Q"), - ("IORegistryEntrySearchCFProperty", b"@Ir*" + CFStringRef + CFAllocatorRef + b"I"), - ("IORegistryEntryCreateCFProperty", b"@I" + CFStringRef + CFAllocatorRef + b"I"), - ("IORegistryEntryGetParentEntry", b"IIr*o^I"), - ("IOServiceGetMatchingServices", b"II" + CFDictionaryRef + b"o^I"), - ("IORegistryEntryIDMatching", CFDictionaryRef + b"Q"), - ("IORegistryEntryFromPath", b"II" + const_io_name_t_ref_in), - ("IORegistryEntryGetPath", b"II" + const_io_name_t_ref_in + b"o" + io_name_t_ref_out), - ("IOServiceNameMatching", CFDictionaryRef + b"r*"), - ("IOObjectConformsTo", b"II" + const_io_name_t_ref_in), - ("IOServiceMatching", CFDictionaryRef + b"r*"), - ("IOObjectRelease", b"II"), - ("IOIteratorNext", b"II"), -] - -kIOServicePlane = b"IOService" - -variables = [("kIOMasterPortDefault", b"I")] - -pointer = type(None) - -kern_return_t = NewType("kern_return_t", int) -boolean_t = int - -io_object_t = NewType("io_object_t", object) -io_name_t = bytes -io_string_t = bytes - -io_registry_entry_t = io_object_t -io_iterator_t = NewType("io_iterator_t", io_object_t) - -CFTypeRef = Union[int, float, bytes, dict, list] - -IOOptionBits = int -mach_port_t = int -CFAllocatorType = type(kCFAllocatorDefault) - -NULL = 0 - -kIOMasterPortDefault: mach_port_t = NULL -kNilOptions: IOOptionBits = NULL - -# IOKitLib.h -kIORegistryIterateRecursively: IOOptionBits = 1 -kIORegistryIterateParents: IOOptionBits = 2 - - -# kern_return_t -# IORegistryEntryCreateCFProperties ( -# io_registry_entry_t entry, -# CFMutableDictionaryRef *properties, -# CFAllocatorRef allocator, -# IOOptionBits options -# ); -def IORegistryEntryCreateCFProperties( - entry: io_registry_entry_t, - properties, - allocator: CFAllocatorType, - options: IOOptionBits -) -> kern_return_t: - raise NotImplementedError - - -# kern_return_t IORegistryEntryGetLocationInPlane(io_registry_entry_t entry, const io_name_t plane, io_name_t location); -def IORegistryEntryGetLocationInPlane( - entry: io_registry_entry_t, - plane: io_name_t, - location: io_name_t -) -> kern_return_t: - raise NotImplementedError - - -# CFTypeRef IORegistryEntrySearchCFProperty(io_registry_entry_t entry, const char *plane, CFStringRef key, CFAllocatorRef allocator, IOOptionBits options); -def IORegistryEntrySearchCFProperty( - entry: io_registry_entry_t, - plane: io_name_t, - key: str, - allocator: CFAllocatorType, - options: IOOptionBits -) -> CFTypeRef: - raise NotImplementedError - - -# CFTypeRef IORegistryEntryCreateCFProperty(io_registry_entry_t entry, CFStringRef key, CFAllocatorRef allocator, IOOptionBits options); -def IORegistryEntryCreateCFProperty(entry, key, allocator, options): - raise NotImplementedError - - -# kern_return_t IORegistryEntryGetParentEntry(io_registry_entry_t entry, const char *plane, io_registry_entry_t parent); -def IORegistryEntryGetParentEntry(entry, plane, parent): - raise NotImplementedError - - -# kern_return_t IORegistryEntryGetRegistryEntryID(io_registry_entry_t entry, uint64_t * entryID); -def IORegistryEntryGetRegistryEntryID(entry, entryID): - raise NotImplementedError - - -# io_registry_entry_t IORegistryEntryFromPath(mach_port_t masterPort, io_name_t path); -def IORegistryEntryFromPath(masterPort, path): - raise NotImplementedError - - -# kern_return_t IORegistryGetPath(io_registry_entry_t entry, const io_name_t plane, io_string_t path); -def IORegistryEntryGetPath(entry, plane, path): - raise NotImplementedError - - -# kern_return_t IOServiceGetMatchingServices(mach_port_t masterPort, CFDictionaryRef matching CF_RELEASES_ARGUMENT, io_iterator_t * existing); -def IOServiceGetMatchingServices(masterPort, matching, existing): - raise NotImplementedError - - -# CFMutableDictionaryRef IORegistryEntryIDMatching(uint64_t entryID); -def IORegistryEntryIDMatching(entryID): - raise NotImplementedError - - -# CFMutableDictionaryRef IOServiceNameMatching(const char * name); -def IOServiceNameMatching(name): - raise NotImplementedError - - -# CFMutableDictionaryRef IOServiceMatching(const char * name); -def IOServiceMatching(name): - raise NotImplementedError - - -# boolean_t IOObjectConformsTo(io_object_t object, const char *className); -def IOObjectConformsTo(object, className): - raise NotImplementedError - - -# kern_return_t IOObjectRelease(io_object_t object); -def IOObjectRelease(object): - raise NotImplementedError - - -# io_object_t IOIteratorNext(io_iterator_t iterator); -def IOIteratorNext(iterator): - raise NotImplementedError - - -objc.loadBundleFunctions(IOKit, globals(), functions) -objc.loadBundleVariables(IOKit, globals(), variables) - - -def ioiterator_to_list(iterator): - item = IOIteratorNext(iterator) - - while item: - yield item - item = IOIteratorNext(iterator) - - IOObjectRelease(item) - - -def corefoundation_to_native(collection): - if collection is None: # nullptr - return None - - native = Conversion.pythonCollectionFromPropertyList(collection) - - CFRelease(collection) - - return native - - -def ioname_t_to_str(name): - return name.partition(b"\0")[0].decode() diff --git a/src/hwprobe/core/mac/display.py b/src/hwprobe/core/mac/display.py index 9d6ea86..2d01169 100644 --- a/src/hwprobe/core/mac/display.py +++ b/src/hwprobe/core/mac/display.py @@ -25,8 +25,7 @@ def _get_monitor_resolution_from_system_profiler(monitor_info: dict) -> Optional def _enrich_data_from_edid(monitor_info: DisplayModuleInfo, edid_string: str) -> DisplayModuleInfo: - if edid_string.lower().startswith("0x"): - edid_string = edid_string[2:] + edid_string = edid_string.lower().removeprefix("0x") edid_bytes = bytes.fromhex(edid_string) data: DisplayModuleInfo = parse_edid(edid_bytes) for field in data.model_dump().keys(): diff --git a/src/hwprobe/core/mac/graphics.py b/src/hwprobe/core/mac/graphics.py index 2b3346b..bea917c 100644 --- a/src/hwprobe/core/mac/graphics.py +++ b/src/hwprobe/core/mac/graphics.py @@ -100,105 +100,3 @@ def fetch_graphics_info() -> GraphicsInfo: return graphics_info - -""" -This older fetch_graphics_info uses pyobjc to connect to IOKit. -This has been replaced by means of C++ bindings to the dylib. -Refer `src/interops/mac`. - -def old_fetch_graphics_info() -> GraphicsInfo: - - graphics_info = GraphicsInfo() - is_arm = check_arm() - - if not is_arm: - # x86 machines enumerate their GPUs differently - device = { - "IOProviderClass": "IOPCIDevice", - # Bit mask matching, ensuring that the 3rd byte is one of the display controller (0x03). - "IOPCIClassMatch": "0x03000000&0xff000000", - } - else: - device = {"IONameMatched": "gpu*"} - - interface = ioiterator_to_list( - IOServiceGetMatchingServices(kIOMasterPortDefault, device, None)[1] - ) - - if not interface: - graphics_info.status.type = StatusType.FAILED - graphics_info.status.messages.append("Could not enumerate GPUs") - return graphics_info - - for i in interface: - device = corefoundation_to_native( - IORegistryEntryCreateCFProperties( - i, None, kCFAllocatorDefault, kNilOptions - ) - )[1] - - try: - # For Apple's M1 iGFX - if ( - is_arm - and - # If both return true, that means - # we aren't dealing with a GPU device. - not "gpu" in device.get("IONameMatched", "").lower() - and not "AGX" in device.get("CFBundleIdentifierKernel", "") - ): - continue - except: - continue - - model = device.get("model", None) - if not model: - continue - - gpu = GPUInfo() - gpu.name = model - - try: - gpu.vendor_id = "0x" + ( - binascii.b2a_hex(bytes(reversed(device.get("vendor-id")))).decode()[ - 4: - ] - ) - - if not is_arm: - gpu.device_id = "0x" + ( - binascii.b2a_hex( - bytes(reversed(device.get("device-id"))) - ).decode()[4:] - ) - # todo: get VRAM for non-ARM devices - else: - gpu_config = device.get("GPUConfigurationVariable", {}) - gpu.apple_gpu_core_count = gpu_config.get("num_cores") - gpu.apple_neural_core_count = gpu_config.get("num_gps") - gpu.manufacturer = "Apple Inc." - gpu.subsystem_manufacturer = "Apple Inc." - # We use subsystem_model for the gpu generation - gpu.subsystem_model = str(gpu_config.get("gpu_gen")) if gpu_config.get("gpu_gen") else None - - memory = subprocess.run(["sysctl", "hw.memsize"], capture_output=True).stdout.decode("utf-8") - memory = memory.split(":")[1].strip() - if memory.isnumeric(): - gpu.vram = Megabyte(capacity=int(memory) // (1024 ** 2)) - - # Now we get the ACPI path for x86 devices - if not is_arm: - data = construct_pci_path_mac( - i, device.get("acpi-path", "") - ) - gpu.pci_path = data.get("pci_path") - gpu.acpi_path = data.get("acpi_path") - - graphics_info.modules.append(gpu) - - except Exception as e: - graphics_info.status.type = StatusType.PARTIAL - graphics_info.status.messages.append(f"Failed to enumerate GPU: {e}") - - return graphics_info -""" diff --git a/src/hwprobe/core/mac/memory.py b/src/hwprobe/core/mac/memory.py index d397ed7..872e014 100644 --- a/src/hwprobe/core/mac/memory.py +++ b/src/hwprobe/core/mac/memory.py @@ -78,19 +78,16 @@ def get_arm_ram_info() -> MemoryInfo: def get_ram_size_from_system_profiler() -> List[StorageSize]: sizes = [] - try: - value = subprocess.check_output(["system_profiler", "SPMemoryDataType", "-xml"]) - pl = plistlib.loads(value, fmt=plistlib.FMT_XML) - for entry in pl: - items = entry["_items"] - for item in items: - sticks = item["_items"] - for stick in sticks: - size = stick["dimm_size"] - if size: - sizes.append(int(size.removesuffix(" GB"))) - except Exception: - raise + value = subprocess.check_output(["system_profiler", "SPMemoryDataType", "-xml"]) + pl = plistlib.loads(value, fmt=plistlib.FMT_XML) + for entry in pl: + items = entry.get("_items") + for item in items: + sticks = item.get("_items") + for stick in sticks: + size = stick.get("dimm_size") + if size: + sizes.append(int(size.removesuffix(" GB"))) return [Gigabyte(capacity=x) for x in sizes] diff --git a/src/hwprobe/core/mac/network.py b/src/hwprobe/core/mac/network.py index 3825a96..330c2ed 100644 --- a/src/hwprobe/core/mac/network.py +++ b/src/hwprobe/core/mac/network.py @@ -80,10 +80,14 @@ def _get_bsd_interface_apple_silicon(item: dict, driver: str = "AppleBCMWLANCore Tries the AppleBCMWLANCore path first, then falls back to the AppleWLANDriver (Wi-Fi 7 / Skywalk STA) path. """ - if driver == "AppleBCMWLANCore": - return _traverse_ioreg(item, _STEPS_BCM_WLAN) - elif driver == "AppleWLANDriver": - _traverse_ioreg(item, _STEPS_WLAN_DRIVER) + mapping = { + "AppleBCMWLANCore": _STEPS_BCM_WLAN, + "AppleWLANDriver": _STEPS_WLAN_DRIVER, + # Add to this if more drivers are supported + } + + if driver in mapping: + return _traverse_ioreg(item, mapping[driver]) return ( _traverse_ioreg(item, _STEPS_BCM_WLAN) @@ -101,12 +105,12 @@ def _fetch_airport_details() -> Dict[str, NICInfo]: res = {} - for item in plist: - io_name_pattern = re.compile(r"pci([0-9a-fA-F]{4}),([0-9a-fA-F]{4})") + io_name_pattern = re.compile(r"pci([0-9a-fA-F]{4}),([0-9a-fA-F]{4})") + for item in plist: driver = item.get("IORegistryEntryName") - if not driver: return res + if not driver: continue if driver == "AirPort_BrcmNIC": # Intel Macs, usually diff --git a/src/hwprobe/core/windows/baseboard.py b/src/hwprobe/core/windows/baseboard.py index 8495aa3..26f49ad 100644 --- a/src/hwprobe/core/windows/baseboard.py +++ b/src/hwprobe/core/windows/baseboard.py @@ -15,7 +15,7 @@ def fetch_baseboard_info() -> BaseboardInfo: result = FetchSMBIOSData(byref(info)) if result != STATUS_OK: - baseboard_info.status.type = StatusType.FAILURE + baseboard_info.status.type = StatusType.FAILED baseboard_info.status.messages.append("Failed to fetch SMBIOS hardware info for Baseboard") return baseboard_info diff --git a/src/hwprobe/core/windows/storage.py b/src/hwprobe/core/windows/storage.py index 927fdd6..4b2dfe2 100644 --- a/src/hwprobe/core/windows/storage.py +++ b/src/hwprobe/core/windows/storage.py @@ -48,8 +48,6 @@ def fetch_wmi_storage_info() -> StorageInfo: manufacturer = props.get("Manufacturer") model = props.get("Model") - print(manufacturer) - disk.model = ( model.strip() if model else friendly_name.strip() if friendly_name else None ) diff --git a/src/hwprobe/util/location_paths.py b/src/hwprobe/util/location_paths.py index 67a0ce6..625ad2e 100644 --- a/src/hwprobe/util/location_paths.py +++ b/src/hwprobe/util/location_paths.py @@ -9,7 +9,7 @@ c_wchar_p, sizeof, ) -from typing import Tuple +from typing import List, Optional, Tuple cfgmgr = WinDLL("cfgmgr32.dll") @@ -142,7 +142,7 @@ def CM_Get_DevNode_PropertyW( return (propType, propBuff, propBuffSize) -def decode_location_paths(raw_bytes: bytes) -> list[str]: +def decode_location_paths(raw_bytes: bytes) -> List[str]: """ Decode the raw location paths bytes into a list of strings. @@ -159,7 +159,7 @@ def decode_location_paths(raw_bytes: bytes) -> list[str]: return paths -def decode_uint32(raw_bytes: bytes) -> int | None: +def decode_uint32(raw_bytes: bytes) -> Optional[int]: """ Decode a 32-bit unsigned integer from raw bytes. @@ -175,7 +175,7 @@ def decode_uint32(raw_bytes: bytes) -> int | None: return None -def _fetch_property(pnp_device_id: str, key_def: list): +def _fetch_property(pnp_device_id: str, key_def: list): # type: ignore[type-arg] """ Generic property fetcher using CM_Get_DevNode_PropertyW. @@ -203,7 +203,7 @@ def _fetch_property(pnp_device_id: str, key_def: list): return CM_Get_DevNode_PropertyW(dnDevInst, dpKey) -def get_location_paths(pnp_device_id: str) -> list[str] | None: +def get_location_paths(pnp_device_id: str) -> Optional[List[str]]: """ Get the location paths for a PNP device. @@ -222,7 +222,7 @@ def get_location_paths(pnp_device_id: str) -> list[str] | None: return decode_location_paths(raw_bytes) -def get_bus_number(pnp_device_id: str) -> str | None: +def get_bus_number(pnp_device_id: str) -> Optional[str]: """ Get the bus number for a PNP device. @@ -242,7 +242,7 @@ def get_bus_number(pnp_device_id: str) -> str | None: return str(value) if value is not None else None -def get_device_address(pnp_device_id: str) -> str | None: +def get_device_address(pnp_device_id: str) -> Optional[str]: """ Get the device address for a PNP device. @@ -262,7 +262,7 @@ def get_device_address(pnp_device_id: str) -> str | None: return str(value) if value is not None else None -def get_pcie_link_speed(pnp_device_id: str) -> int | None: +def get_pcie_link_speed(pnp_device_id: str) -> Optional[int]: result = _fetch_property(pnp_device_id, pcie_link_speed_key) if result is None: @@ -271,7 +271,7 @@ def get_pcie_link_speed(pnp_device_id: str) -> int | None: return decode_uint32(raw_bytes) -def get_pcie_link_width(pnp_device_id: str) -> int | None: +def get_pcie_link_width(pnp_device_id: str) -> Optional[int]: result = _fetch_property(pnp_device_id, pcie_link_width_key) if result is None: return None @@ -281,7 +281,7 @@ def get_pcie_link_width(pnp_device_id: str) -> int | None: def fetch_device_properties( pnp_device_id: str, -) -> tuple[list[str] | None, str | None, str | None]: +) -> Tuple[Optional[List[str]], Optional[str], Optional[str]]: """ Fetch location paths, bus number, and device address in one call. @@ -298,7 +298,7 @@ def fetch_device_properties( ) -def fetch_pcie_info(pnp_device_id: str) -> Tuple[str] | None: +def fetch_pcie_info(pnp_device_id: str) -> Optional[Tuple[Optional[int], Optional[int]]]: """ Fetch PCIe link speed and width for a PNP device. From 4808e8f6d4cabc2af44e4da47939e3ece0248b25 Mon Sep 17 00:00:00 2001 From: Mahasvan Date: Sat, 1 Aug 2026 15:13:52 +0530 Subject: [PATCH 02/10] Ruff fixes --- pyproject.toml | 4 ++++ src/hwprobe/core/linux/cpu.py | 4 ++-- src/hwprobe/core/linux/display.py | 2 +- src/hwprobe/core/linux/graphics.py | 2 +- src/hwprobe/core/linux/memory.py | 6 +++--- src/hwprobe/core/linux/network.py | 6 +++--- src/hwprobe/core/linux/storage.py | 29 ++++++++++++++-------------- src/hwprobe/core/mac/display.py | 4 ++-- src/hwprobe/core/mac/graphics.py | 3 +-- src/hwprobe/core/mac/memory.py | 5 ++--- src/hwprobe/core/mac/network.py | 16 +++++++-------- src/hwprobe/core/mac/storage.py | 3 +-- src/hwprobe/core/windows/cpu.py | 3 +-- src/hwprobe/core/windows/memory.py | 3 +-- src/hwprobe/models/audio_models.py | 6 +++--- src/hwprobe/models/cpu_models.py | 4 ++-- src/hwprobe/models/display_models.py | 4 ++-- src/hwprobe/models/gpu_models.py | 4 ++-- src/hwprobe/models/memory_models.py | 4 ++-- src/hwprobe/models/network_models.py | 4 ++-- src/hwprobe/models/status_models.py | 3 +-- src/hwprobe/models/storage_models.py | 4 ++-- src/hwprobe/util/location_paths.py | 10 +++++----- src/hwprobe/util/nvidia.py | 3 +-- 24 files changed, 66 insertions(+), 70 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 02c85a3..a0b72fe 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,3 +37,7 @@ where = ["src"] [project.urls] Homepage = "https://github.com/Mahasvan/HWProbe" Issues = "https://github.com/Mahasvan/HWProbe/issues" + +[tool.ruff] +target-version = "py39" +line-length = 120 diff --git a/src/hwprobe/core/linux/cpu.py b/src/hwprobe/core/linux/cpu.py index 9e9da50..b5dadd6 100644 --- a/src/hwprobe/core/linux/cpu.py +++ b/src/hwprobe/core/linux/cpu.py @@ -1,6 +1,6 @@ import re import subprocess -from typing import Optional, List +from typing import Optional from hwprobe.models.cpu_models import CPUInfo from hwprobe.models.status_models import StatusType @@ -59,7 +59,7 @@ def _cpu_threads(raw_cpu_info: str) -> Optional[int]: return None -def _x86_flags(cpu_lines: str) -> Optional[List[str]]: +def _x86_flags(cpu_lines: str) -> Optional[list[str]]: flags_match = re.search(r"flags\s+:\s+(.+)", cpu_lines) if not flags_match: return None diff --git a/src/hwprobe/core/linux/display.py b/src/hwprobe/core/linux/display.py index 9d6ee77..04d479e 100644 --- a/src/hwprobe/core/linux/display.py +++ b/src/hwprobe/core/linux/display.py @@ -63,7 +63,7 @@ def _fetch_individual_monitor_info(device_path: str) -> Optional[DisplayModuleIn acpi_file = os.path.join(device_path, "firmware_node", "path") if os.path.exists(acpi_file): - with open(acpi_file, "r") as f: + with open(acpi_file) as f: monitor_data.acpi_path = f.read().strip() return monitor_data diff --git a/src/hwprobe/core/linux/graphics.py b/src/hwprobe/core/linux/graphics.py index a3e0e54..2e16d03 100644 --- a/src/hwprobe/core/linux/graphics.py +++ b/src/hwprobe/core/linux/graphics.py @@ -39,7 +39,7 @@ def _pcie_gen(device) -> Optional[int]: return None try: - with open(path, "r") as f: + with open(path) as f: raw_speed = f.read().strip() # e.g., "16.0 GT/s" # Mapping Dictionary diff --git a/src/hwprobe/core/linux/memory.py b/src/hwprobe/core/linux/memory.py index bbaa17f..ff36a4c 100644 --- a/src/hwprobe/core/linux/memory.py +++ b/src/hwprobe/core/linux/memory.py @@ -1,5 +1,5 @@ import os -from typing import Optional, List +from typing import Optional from hwprobe.core.linux.dmi_decode import get_string_entry, MEMORY_TYPE from hwprobe.models.memory_models import MemoryInfo, MemoryModuleSlot, MemoryModuleInfo @@ -10,7 +10,7 @@ # Thank you to [Quist](https://github.com/nadiaholmquist) for helping with our understanding of this. -def _part_no(strings: List[bytes], value: bytes) -> Optional[str]: +def _part_no(strings: list[bytes], value: bytes) -> Optional[str]: """ Obtains the value at offset 1Ah, which indicates at which index, pre-sanitization, in the `strings` list the real string value is stored. @@ -29,7 +29,7 @@ def _dimm_type(value: bytes) -> Optional[str]: return MEMORY_TYPE.get(value[0x12]) -def _dimm_slot(strings: List[bytes], value: bytes) -> Optional[MemoryModuleSlot]: +def _dimm_slot(strings: list[bytes], value: bytes) -> Optional[MemoryModuleSlot]: return MemoryModuleSlot( channel=get_string_entry(strings, value[0x10]), bank=get_string_entry(strings, value[0x11]) diff --git a/src/hwprobe/core/linux/network.py b/src/hwprobe/core/linux/network.py index fcdcc9d..b5043a6 100644 --- a/src/hwprobe/core/linux/network.py +++ b/src/hwprobe/core/linux/network.py @@ -22,20 +22,20 @@ def _enrich_with_sysfs_info(nic: NICInfo, status: Status) -> None: raise ValueError(f"Interface is virtual: {interface_name}") try: - with open(f"{base_path}/vendor", "r") as f: + with open(f"{base_path}/vendor") as f: nic.vendor_id = f.read().strip() # todo: Manufacturer except FileNotFoundError: status.make_partial(f"Vendor ID not found for interface {interface_name}") try: - with open(f"{base_path}/device", "r") as f: + with open(f"{base_path}/device") as f: nic.device_id = f.read().strip() except FileNotFoundError: status.make_partial(f"Device ID not found for interface {interface_name}") try: - with open(f"{base_path}/firmware_node/path", "r") as f: + with open(f"{base_path}/firmware_node/path") as f: nic.acpi_path = f.read().strip() except FileNotFoundError: status.make_partial(f"Path not found for interface {interface_name}") diff --git a/src/hwprobe/core/linux/storage.py b/src/hwprobe/core/linux/storage.py index 96ff2db..a0f71e6 100644 --- a/src/hwprobe/core/linux/storage.py +++ b/src/hwprobe/core/linux/storage.py @@ -1,12 +1,11 @@ import os -from typing import Tuple from hwprobe.models.size_models import Megabyte from hwprobe.models.status_models import StatusType, Status from hwprobe.models.storage_models import StorageInfo, DiskInfo -def _fetch_emmc_info(folder: str) -> Tuple[DiskInfo, Status]: +def _fetch_emmc_info(folder: str) -> tuple[DiskInfo, Status]: """ Helper function for eMMC devices, which have different places to get some data. @@ -19,13 +18,13 @@ def _fetch_emmc_info(folder: str) -> Tuple[DiskInfo, Status]: disk.identifier = folder.strip() - model = open(f"{path}/device/name", "r").read().strip() + model = open(f"{path}/device/name").read().strip() disk.model = model if not model: status.type = StatusType.PARTIAL status.messages.append("Disk Model could not be found") - removable = open(f"{path}/removable", "r").read().strip() + removable = open(f"{path}/removable").read().strip() if removable == "0": disk.type = "Embedded MultiMediaCard (eMMC)" @@ -35,26 +34,26 @@ def _fetch_emmc_info(folder: str) -> Tuple[DiskInfo, Status]: disk.location = "Internal" if removable == "0" else "External" disk.connector = "Unknown" - vendor_id = open(f"{path}/device/manfid", "r").read().strip() + vendor_id = open(f"{path}/device/manfid").read().strip() disk.vendor_id = vendor_id if not vendor_id: status.type = StatusType.PARTIAL status.messages.append("Disk vendor id could not be found") - device_id = open(f"{path}/device/oemid", "r").read().strip() + device_id = open(f"{path}/device/oemid").read().strip() disk.device_id = device_id if not device_id: status.type = StatusType.PARTIAL status.messages.append("Disk device id could not be found") - size = open(f"{path}/size", "r").read().strip() + size = open(f"{path}/size").read().strip() size_in_bytes = int(size) * 512 disk.size = Megabyte(capacity=(size_in_bytes // 1024 ** 2)) return disk, status -def _fetch_standard_disk_info(folder: str) -> Tuple[DiskInfo, Status]: +def _fetch_standard_disk_info(folder: str) -> tuple[DiskInfo, Status]: """ Helper function for NVMe and SATA (sd*,nvme*) storage devices. @@ -67,15 +66,15 @@ def _fetch_standard_disk_info(folder: str) -> Tuple[DiskInfo, Status]: disk.identifier = folder.strip() - model = open(f"{path}/device/model", "r").read().strip() + model = open(f"{path}/device/model").read().strip() if model: disk.model = model else: status.type = StatusType.PARTIAL status.messages.append("Disk Model could not be found") - rotational = open(f"{path}/queue/rotational", "r").read().strip() - removable = open(f"{path}/removable", "r").read().strip() + rotational = open(f"{path}/queue/rotational").read().strip() + removable = open(f"{path}/removable").read().strip() disk.type = ( "Solid State Drive (SSD)" @@ -87,15 +86,15 @@ def _fetch_standard_disk_info(folder: str) -> Tuple[DiskInfo, Status]: if "nvme" in folder: disk.connector = "PCIe" disk.type = "Non-Volatile Memory Express (NVMe)" - disk.device_id = open(f"{path}/device/device/device", "r").read().strip() - disk.vendor_id = open(f"{path}/device/device/vendor", "r").read().strip() + disk.device_id = open(f"{path}/device/device/device").read().strip() + disk.vendor_id = open(f"{path}/device/device/vendor").read().strip() elif "sd" in folder: disk.connector = "SCSI" - disk.vendor_id = open(f"{path}/device/vendor", "r").read().strip() + disk.vendor_id = open(f"{path}/device/vendor").read().strip() else: disk.connector = "Unknown" - size = open(f"{path}/size", "r").read().strip() + size = open(f"{path}/size").read().strip() size_in_bytes = int(size) * 512 disk.size = Megabyte(capacity=(size_in_bytes // 1024 ** 2)) diff --git a/src/hwprobe/core/mac/display.py b/src/hwprobe/core/mac/display.py index 2d01169..806498a 100644 --- a/src/hwprobe/core/mac/display.py +++ b/src/hwprobe/core/mac/display.py @@ -1,13 +1,13 @@ import json import re import subprocess -from typing import Tuple, Optional +from typing import Optional from hwprobe.core.common.edid import parse_edid from hwprobe.models.display_models import DisplayInfo, DisplayModuleInfo, ResolutionInfo -def _get_monitor_resolution_from_system_profiler(monitor_info: dict) -> Optional[Tuple[int, int]]: +def _get_monitor_resolution_from_system_profiler(monitor_info: dict) -> Optional[tuple[int, int]]: precedence = [ "spdisplays_pixelresolution", "spdisplays_resolution", diff --git a/src/hwprobe/core/mac/graphics.py b/src/hwprobe/core/mac/graphics.py index bea917c..20b0251 100644 --- a/src/hwprobe/core/mac/graphics.py +++ b/src/hwprobe/core/mac/graphics.py @@ -1,4 +1,3 @@ -from typing import List from hwprobe.models.gpu_models import GraphicsInfo, GPUInfo, AppleExtendedGPUInfo from hwprobe.models.size_models import Megabyte @@ -24,7 +23,7 @@ def fetch_graphics_info() -> GraphicsInfo: # and RuntimeError at call time if the C library returns -1 try: from hwprobe.interops.mac.bindings.gpu_info import get_gpu_info, GPUProperties - gpu_list: List[GPUProperties] = get_gpu_info() + gpu_list: list[GPUProperties] = get_gpu_info() except FileNotFoundError as e: graphics_info.status.type = StatusType.FAILED diff --git a/src/hwprobe/core/mac/memory.py b/src/hwprobe/core/mac/memory.py index 872e014..123c7a6 100644 --- a/src/hwprobe/core/mac/memory.py +++ b/src/hwprobe/core/mac/memory.py @@ -1,13 +1,12 @@ import plistlib import subprocess -from typing import List from hwprobe.models.memory_models import MemoryInfo, MemoryModuleInfo, MemoryModuleSlot from hwprobe.models.size_models import Megabyte, StorageSize, Gigabyte from hwprobe.models.status_models import StatusType -def get_ram_size_from_reg(reg) -> List[StorageSize]: +def get_ram_size_from_reg(reg) -> list[StorageSize]: """ Observed values of reg: "02 00 00 00 00 00 00 00 02 00 00 00 00 00 00 00" -> Two sticks of 4GB each @@ -76,7 +75,7 @@ def get_arm_ram_info() -> MemoryInfo: return memory_info -def get_ram_size_from_system_profiler() -> List[StorageSize]: +def get_ram_size_from_system_profiler() -> list[StorageSize]: sizes = [] value = subprocess.check_output(["system_profiler", "SPMemoryDataType", "-xml"]) pl = plistlib.loads(value, fmt=plistlib.FMT_XML) diff --git a/src/hwprobe/core/mac/network.py b/src/hwprobe/core/mac/network.py index 330c2ed..c4a3452 100644 --- a/src/hwprobe/core/mac/network.py +++ b/src/hwprobe/core/mac/network.py @@ -1,18 +1,18 @@ import plistlib import re import subprocess -from typing import List, Dict, Optional +from typing import Optional from hwprobe.models.network_models import NetworkInfo, NICInfo -def _fetch_controllers() -> List[str]: +def _fetch_controllers() -> list[str]: output = subprocess.run(["ipconfig", "getiflist"], capture_output=True) stripped = output.stdout.decode("utf-8").strip() return stripped.split(" ") if stripped else [] -def _fetch_ethernet_details() -> Dict[str, NICInfo]: +def _fetch_ethernet_details() -> dict[str, NICInfo]: output = subprocess.run(["system_profiler", "SPEthernetDataType", "-xml"], capture_output=True) plist = plistlib.loads(output.stdout) res = {} @@ -35,7 +35,7 @@ def _find_child(children: list, key: str, value: str) -> Optional[dict]: return next((x for x in children if x and x.get(key) == value), None) -def _traverse_ioreg(root: dict, steps: List[tuple], result_key: str = "IORegistryEntryName") -> Optional[str]: +def _traverse_ioreg(root: dict, steps: list[tuple], result_key: str = "IORegistryEntryName") -> Optional[str]: """ Generic IORegistry depth-first traversal. @@ -95,7 +95,7 @@ def _get_bsd_interface_apple_silicon(item: dict, driver: str = "AppleBCMWLANCore ) -def _fetch_airport_details() -> Dict[str, NICInfo]: +def _fetch_airport_details() -> dict[str, NICInfo]: """ Earlier, `system_profiler SPAirPortDataType -xml` was used to get the vendor and device id. However, this was too slow, and we can get the same details from `ioreg`, while it being faster. @@ -195,13 +195,13 @@ def _fetch_airport_details() -> Dict[str, NICInfo]: return res -def _fetch_system_profiler_details(valid_bsd_interfaces: List[str]) -> NetworkInfo: +def _fetch_system_profiler_details(valid_bsd_interfaces: list[str]) -> NetworkInfo: output = subprocess.run(["system_profiler", "SPNetworkDataType", "-xml"], capture_output=True) plist = plistlib.loads(output.stdout) network_info = NetworkInfo() - ethernet_info: Optional[Dict[str, NICInfo]] = None - airport_info: Optional[Dict[str, NICInfo]] = None + ethernet_info: Optional[dict[str, NICInfo]] = None + airport_info: Optional[dict[str, NICInfo]] = None for item in plist: for network_controller in item.get("_items", []): diff --git a/src/hwprobe/core/mac/storage.py b/src/hwprobe/core/mac/storage.py index e9e5c26..ecff035 100644 --- a/src/hwprobe/core/mac/storage.py +++ b/src/hwprobe/core/mac/storage.py @@ -1,4 +1,3 @@ -from typing import List from hwprobe.models.size_models import Megabyte from hwprobe.models.status_models import StatusType @@ -20,7 +19,7 @@ def fetch_storage_info() -> StorageInfo: try: from hwprobe.interops.mac.bindings.storage_info import get_storage_info, StorageDeviceProperties - disk_list: List[StorageDeviceProperties] = get_storage_info() + disk_list: list[StorageDeviceProperties] = get_storage_info() except FileNotFoundError as e: storage_info.status.type = StatusType.FAILED diff --git a/src/hwprobe/core/windows/cpu.py b/src/hwprobe/core/windows/cpu.py index deb15e2..6679bc5 100644 --- a/src/hwprobe/core/windows/cpu.py +++ b/src/hwprobe/core/windows/cpu.py @@ -2,7 +2,6 @@ import os import winreg from ctypes import wintypes -from typing import List from hwprobe.core.windows.win_enum import FEATURE_ID_MAP from hwprobe.models.cpu_models import CPUInfo @@ -44,7 +43,7 @@ def get_arm_version() -> str: return "7 or lower" -def get_features() -> List[str]: +def get_features() -> list[str]: """ We use the Win32 API function IsProcessorFeaturePresent to check for SSE features. https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-isprocessorfeaturepresent diff --git a/src/hwprobe/core/windows/memory.py b/src/hwprobe/core/windows/memory.py index dcfd8fc..cebe1f8 100644 --- a/src/hwprobe/core/windows/memory.py +++ b/src/hwprobe/core/windows/memory.py @@ -1,5 +1,4 @@ import ctypes -from typing import Tuple from hwprobe.core.windows.win_enum import ECC_MEMORY_TYPE, MEMORY_TYPE # todo: refactor to new bindings @@ -14,7 +13,7 @@ from hwprobe.models.status_models import StatusType -def check_ecc() -> Tuple[bool, str]: +def check_ecc() -> tuple[bool, str]: """ Checks if the system supports ECC memory by querying Win32_PhysicalMemoryArray. diff --git a/src/hwprobe/models/audio_models.py b/src/hwprobe/models/audio_models.py index f3a0743..c94af4a 100644 --- a/src/hwprobe/models/audio_models.py +++ b/src/hwprobe/models/audio_models.py @@ -1,4 +1,4 @@ -from typing import List, Optional +from typing import Optional from hwprobe.models.component_model import ComponentInfo, BaseModel from pydantic import Field @@ -31,11 +31,11 @@ class AudioControllerInfo(BaseModel): manufacturer: Optional[str] = None #: The list of audio endpoints associated with this controller - endpoints: List[AudioDeviceInfo] = Field(default_factory=list) + endpoints: list[AudioDeviceInfo] = Field(default_factory=list) class AudioInfo(ComponentInfo): """This is the model that holds audio information.""" #: The list of audio controllers / modules present on the system - modules: List[AudioControllerInfo] = Field(default_factory=list) + modules: list[AudioControllerInfo] = Field(default_factory=list) diff --git a/src/hwprobe/models/cpu_models.py b/src/hwprobe/models/cpu_models.py index 1059505..e7676d9 100644 --- a/src/hwprobe/models/cpu_models.py +++ b/src/hwprobe/models/cpu_models.py @@ -1,4 +1,4 @@ -from typing import List, Optional +from typing import Optional from hwprobe.models.component_model import ComponentInfo from pydantic import Field @@ -22,7 +22,7 @@ class CPUInfo(ComponentInfo): vendor: Optional[str] = None #: SSE flags supported by the CPU. - sse_flags: List[str] = Field(default_factory=list) + sse_flags: list[str] = Field(default_factory=list) #: The number of physical cores present on the CPU cores: Optional[int] = None diff --git a/src/hwprobe/models/display_models.py b/src/hwprobe/models/display_models.py index 84b1bec..04721a3 100644 --- a/src/hwprobe/models/display_models.py +++ b/src/hwprobe/models/display_models.py @@ -1,4 +1,4 @@ -from typing import Optional, List +from typing import Optional from hwprobe.models.component_model import ComponentInfo from pydantic import BaseModel, Field @@ -49,4 +49,4 @@ class DisplayInfo(ComponentInfo): """Contains a list of ``DisplayModuleInfo`` objects.""" #: List of GPU modules present in the system. - modules: List[DisplayModuleInfo] = Field(default_factory=list) + modules: list[DisplayModuleInfo] = Field(default_factory=list) diff --git a/src/hwprobe/models/gpu_models.py b/src/hwprobe/models/gpu_models.py index 80e7b85..2d888d1 100644 --- a/src/hwprobe/models/gpu_models.py +++ b/src/hwprobe/models/gpu_models.py @@ -1,4 +1,4 @@ -from typing import Optional, List +from typing import Optional from hwprobe.models.component_model import ComponentInfo from hwprobe.models.size_models import StorageSize @@ -61,4 +61,4 @@ class GraphicsInfo(ComponentInfo): """Contains list of ``GPUInfo`` objects.""" #: List of GPU modules present in the system. - modules: List[GPUInfo] = Field(default_factory=list) + modules: list[GPUInfo] = Field(default_factory=list) diff --git a/src/hwprobe/models/memory_models.py b/src/hwprobe/models/memory_models.py index 14ca1b9..ffab9d1 100644 --- a/src/hwprobe/models/memory_models.py +++ b/src/hwprobe/models/memory_models.py @@ -1,4 +1,4 @@ -from typing import List, Optional +from typing import Optional from hwprobe.models.component_model import ComponentInfo from hwprobe.models.size_models import StorageSize @@ -29,4 +29,4 @@ class MemoryModuleInfo(BaseModel): class MemoryInfo(ComponentInfo): - modules: List[MemoryModuleInfo] = Field(default_factory=list) + modules: list[MemoryModuleInfo] = Field(default_factory=list) diff --git a/src/hwprobe/models/network_models.py b/src/hwprobe/models/network_models.py index f0917d7..ad14ec8 100644 --- a/src/hwprobe/models/network_models.py +++ b/src/hwprobe/models/network_models.py @@ -1,4 +1,4 @@ -from typing import List, Optional +from typing import Optional from hwprobe.models.component_model import ComponentInfo from pydantic import BaseModel, Field @@ -29,4 +29,4 @@ class NICInfo(BaseModel): class NetworkInfo(ComponentInfo): - modules: List[NICInfo] = Field(default_factory=list) + modules: list[NICInfo] = Field(default_factory=list) diff --git a/src/hwprobe/models/status_models.py b/src/hwprobe/models/status_models.py index 1f7fccf..8b307c0 100644 --- a/src/hwprobe/models/status_models.py +++ b/src/hwprobe/models/status_models.py @@ -1,5 +1,4 @@ from enum import Enum -from typing import List from pydantic import BaseModel, Field @@ -29,7 +28,7 @@ class Status(BaseModel): If the status is ``PARTIAL`` or ``FAILED``, there may be messages that describe the error(s). """ type: StatusType = Field(default_factory=lambda: StatusType.SUCCESS) - messages: List[str] = Field(default_factory=list) + messages: list[str] = Field(default_factory=list) def make_partial(self, message: str = None) -> None: """ diff --git a/src/hwprobe/models/storage_models.py b/src/hwprobe/models/storage_models.py index f77f053..8729b3f 100644 --- a/src/hwprobe/models/storage_models.py +++ b/src/hwprobe/models/storage_models.py @@ -1,4 +1,4 @@ -from typing import List, Optional +from typing import Optional from hwprobe.models.component_model import ComponentInfo from hwprobe.models.size_models import StorageSize @@ -32,4 +32,4 @@ class DiskInfo(BaseModel): class StorageInfo(ComponentInfo): - modules: List[DiskInfo] = Field(default_factory=list) + modules: list[DiskInfo] = Field(default_factory=list) diff --git a/src/hwprobe/util/location_paths.py b/src/hwprobe/util/location_paths.py index 625ad2e..ef1e471 100644 --- a/src/hwprobe/util/location_paths.py +++ b/src/hwprobe/util/location_paths.py @@ -9,7 +9,7 @@ c_wchar_p, sizeof, ) -from typing import List, Optional, Tuple +from typing import Optional cfgmgr = WinDLL("cfgmgr32.dll") @@ -142,7 +142,7 @@ def CM_Get_DevNode_PropertyW( return (propType, propBuff, propBuffSize) -def decode_location_paths(raw_bytes: bytes) -> List[str]: +def decode_location_paths(raw_bytes: bytes) -> list[str]: """ Decode the raw location paths bytes into a list of strings. @@ -203,7 +203,7 @@ def _fetch_property(pnp_device_id: str, key_def: list): # type: ignore[type-arg return CM_Get_DevNode_PropertyW(dnDevInst, dpKey) -def get_location_paths(pnp_device_id: str) -> Optional[List[str]]: +def get_location_paths(pnp_device_id: str) -> Optional[list[str]]: """ Get the location paths for a PNP device. @@ -281,7 +281,7 @@ def get_pcie_link_width(pnp_device_id: str) -> Optional[int]: def fetch_device_properties( pnp_device_id: str, -) -> Tuple[Optional[List[str]], Optional[str], Optional[str]]: +) -> tuple[Optional[list[str]], Optional[str], Optional[str]]: """ Fetch location paths, bus number, and device address in one call. @@ -298,7 +298,7 @@ def fetch_device_properties( ) -def fetch_pcie_info(pnp_device_id: str) -> Optional[Tuple[Optional[int], Optional[int]]]: +def fetch_pcie_info(pnp_device_id: str) -> Optional[tuple[Optional[int], Optional[int]]]: """ Fetch PCIe link speed and width for a PNP device. diff --git a/src/hwprobe/util/nvidia.py b/src/hwprobe/util/nvidia.py index c220346..96b9227 100644 --- a/src/hwprobe/util/nvidia.py +++ b/src/hwprobe/util/nvidia.py @@ -1,8 +1,7 @@ import subprocess -from typing import Tuple -def fetch_gpu_details_nvidia(device: str) -> Tuple[str, int, int, int]: +def fetch_gpu_details_nvidia(device: str) -> tuple[str, int, int, int]: """ :param device: format: ::. :return: GPU name, PCI Width, PCI Gen, Total VRAM in MB From 87b1ba3d4cdddc9fde015fb4b5caec93bc2abf8b Mon Sep 17 00:00:00 2001 From: Mahasvan Date: Sat, 1 Aug 2026 15:15:38 +0530 Subject: [PATCH 03/10] Ruff fixes part 2 --- docs/source/conf.py | 33 +- src/hwprobe/core/common/edid.py | 31 +- src/hwprobe/core/linux/cpu.py | 12 +- src/hwprobe/core/linux/display.py | 26 +- src/hwprobe/core/linux/dmi_decode.py | 2 +- src/hwprobe/core/linux/graphics.py | 29 +- src/hwprobe/core/linux/memory.py | 14 +- src/hwprobe/core/linux/network.py | 3 +- src/hwprobe/core/linux/storage.py | 16 +- src/hwprobe/core/mac/display.py | 3 +- src/hwprobe/core/mac/graphics.py | 11 +- src/hwprobe/core/mac/manager.py | 4 +- src/hwprobe/core/mac/memory.py | 32 +- src/hwprobe/core/mac/network.py | 27 +- src/hwprobe/core/mac/storage.py | 6 +- src/hwprobe/core/windows/audio.py | 4 +- src/hwprobe/core/windows/baseboard.py | 8 +- src/hwprobe/core/windows/common.py | 4 +- src/hwprobe/core/windows/cpu.py | 24 +- src/hwprobe/core/windows/display.py | 99 +++--- src/hwprobe/core/windows/graphics.py | 2 +- src/hwprobe/core/windows/manager.py | 4 +- src/hwprobe/core/windows/memory.py | 15 +- src/hwprobe/core/windows/network.py | 15 +- src/hwprobe/core/windows/storage.py | 30 +- src/hwprobe/core/windows/win_enum.py | 2 +- src/hwprobe/interops/mac/bindings/gpu_info.py | 27 +- .../interops/mac/bindings/storage_info.py | 23 +- src/hwprobe/interops/win/bindings/gpu_info.py | 33 +- src/hwprobe/models/audio_models.py | 3 +- src/hwprobe/models/component_model.py | 3 +- src/hwprobe/models/cpu_models.py | 3 +- src/hwprobe/models/display_models.py | 4 +- src/hwprobe/models/gpu_models.py | 4 +- src/hwprobe/models/info_models.py | 9 +- src/hwprobe/models/memory_models.py | 3 +- src/hwprobe/models/network_models.py | 3 +- src/hwprobe/models/status_models.py | 4 +- src/hwprobe/models/storage_models.py | 3 +- src/hwprobe/util/location_paths.py | 16 +- src/hwprobe/util/nvidia.py | 9 +- tests/core/common/test_edid.py | 24 +- tests/core/linux/test_common.py | 18 +- tests/core/linux/test_cpu.py | 119 ++----- tests/core/linux/test_display.py | 68 ++-- tests/core/linux/test_graphics.py | 51 ++- tests/core/linux/test_memory.py | 103 ++++--- tests/core/linux/test_storage.py | 47 +-- tests/core/mac/test_cpu.py | 53 ++-- tests/core/mac/test_display.py | 203 +++++++----- tests/core/mac/test_graphics.py | 74 +++-- tests/core/mac/test_memory.py | 192 +++++++----- tests/core/mac/test_network.py | 291 ++++++++++-------- tests/core/mac/test_storage.py | 121 ++++---- tests/core/windows/test_display.py | 77 ++--- tests/core/windows/test_graphics.py | 31 +- tests/core/windows/test_network.py | 63 ++-- 57 files changed, 1076 insertions(+), 1062 deletions(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index 58addd0..ca2954a 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -1,7 +1,7 @@ import sys from pathlib import Path -sys.path.insert(0, str(Path('..', 'src').resolve())) +sys.path.insert(0, str(Path("..", "src").resolve())) # Mock platform-specific dependencies so autodoc works off-host autodoc_mock_imports = [] @@ -23,12 +23,7 @@ ] if sys.platform != "darwin": - autodoc_mock_imports += [ - "objc", - "CoreFoundation", - "Foundation", - "PyObjCTools" - ] + autodoc_mock_imports += ["objc", "CoreFoundation", "Foundation", "PyObjCTools"] # Configuration file for the Sphinx documentation builder. # @@ -41,9 +36,9 @@ import hwprobe -project = 'HWProbe' -copyright = '2025, Mahasvan Mohan' -author = 'Mahasvan Mohan' +project = "HWProbe" +copyright = "2025, Mahasvan Mohan" +author = "Mahasvan Mohan" release = hwprobe.__version__ autodoc_class_signature = "separated" @@ -55,25 +50,25 @@ autoclass_content = "class" autodoc_default_options = { - 'member-order': 'bysource', - 'special-members': '__init__', - 'undoc-members': True, - 'exclude-members': '__weakref__, __init___' + "member-order": "bysource", + "special-members": "__init__", + "undoc-members": True, + "exclude-members": "__weakref__, __init___", } # -- General configuration --------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration extensions = [ - 'sphinx.ext.autodoc', - 'sphinxcontrib.autodoc_pydantic', + "sphinx.ext.autodoc", + "sphinxcontrib.autodoc_pydantic", ] -templates_path = ['_templates'] +templates_path = ["_templates"] exclude_patterns = [] # -- Options for HTML output ------------------------------------------------- # https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output -html_theme = 'alabaster' -html_static_path = ['_static'] +html_theme = "alabaster" +html_static_path = ["_static"] diff --git a/src/hwprobe/core/common/edid.py b/src/hwprobe/core/common/edid.py index b760931..d6bfee0 100644 --- a/src/hwprobe/core/common/edid.py +++ b/src/hwprobe/core/common/edid.py @@ -1,13 +1,6 @@ from hwprobe.models.display_models import DisplayModuleInfo, ResolutionInfo -BIT_DEPTH_ENUM = { - 1: 6, - 2: 8, - 3: 10, - 4: 12, - 5: 14, - 6: 16 -} +BIT_DEPTH_ENUM = {1: 6, 2: 8, 3: 10, 4: 12, 5: 14, 6: 16} INTERFACE_ENUM = { 0: "Undefined", @@ -15,7 +8,7 @@ 2: "HDMI", # Standard HDMI-A 3: "HDMI (B)", 4: "MDDI", - 5: "DisplayPort" + 5: "DisplayPort", } DESCRIPTOR_TAG_ENUM = { @@ -72,8 +65,8 @@ def parse_edid(edid_data: bytes) -> DisplayModuleInfo: if input_type >> 7 == 1: # MSB is 1 => Digital output if edid_version >= (1, 4): module.resolution.bit_depth = BIT_DEPTH_ENUM.get( - _get_bits(input_type.to_bytes(1, byteorder="little"), 1, 4), - 0) + _get_bits(input_type.to_bytes(1, byteorder="little"), 1, 4), 0 + ) module.interface = INTERFACE_ENUM.get(input_type & 7, "Unknown") else: module.interface = "Analog" @@ -81,8 +74,8 @@ def parse_edid(edid_data: bytes) -> DisplayModuleInfo: resolution = (0, 0, 0) # Width, Height, Refresh Rate # We will use this tuple to find the max resolution and refresh rate, and update it in `module.resolution`. - for block_start in range(0x36, 0x6d, 18): - block = edid_data[block_start:block_start + 18] + for block_start in range(0x36, 0x6D, 18): + block = edid_data[block_start : block_start + 18] if block[:2] == b"\x00\x00": tag = block[3] if tag in DESCRIPTOR_TAG_ENUM: @@ -94,7 +87,8 @@ def parse_edid(edid_data: bytes) -> DisplayModuleInfo: module.name = block[5:].decode("ascii").strip() else: - if not module.resolution: continue + if not module.resolution: + continue pixel_clock_hz = (block[0] | (block[1] << 8)) * 10_000 @@ -105,11 +99,7 @@ def parse_edid(edid_data: bytes) -> DisplayModuleInfo: v_blank = ((block[7] & 0x0F) << 8) | block[6] refresh_rate = pixel_clock_hz / ((horiz + h_blank) * (vert + v_blank)) - resolution = max( - resolution, - (horiz, vert, round(refresh_rate, 2)), - key=lambda x: (x[0] * x[1], x[2]) - ) + resolution = max(resolution, (horiz, vert, round(refresh_rate, 2)), key=lambda x: (x[0] * x[1], x[2])) if resolution != (0, 0, 0): if not module.resolution: @@ -124,4 +114,5 @@ def parse_edid(edid_data: bytes) -> DisplayModuleInfo: return module -# todo: parse extension blocks \ No newline at end of file + +# todo: parse extension blocks diff --git a/src/hwprobe/core/linux/cpu.py b/src/hwprobe/core/linux/cpu.py index b5dadd6..fbed6b1 100644 --- a/src/hwprobe/core/linux/cpu.py +++ b/src/hwprobe/core/linux/cpu.py @@ -14,7 +14,7 @@ def _arm_cpu_cores() -> Optional[int]: core_ids = [x.split(",")[1] for x in lines] # The number of distinct Core IDs is the number of cores return len(set(core_ids)) - except Exception as e: + except Exception: return None @@ -66,9 +66,7 @@ def _x86_flags(cpu_lines: str) -> Optional[list[str]]: flags = flags_match.group(1) flags = [x.lower().strip() for x in flags.split(" ")] - flags = [ - flag.replace("_", ".").upper() for flag in flags if flag - ] + flags = [flag.replace("_", ".").upper() for flag in flags if flag] return flags @@ -156,11 +154,11 @@ def fetch_cpu_info() -> CPUInfo: # todo: Check if any of the regexes may suffer from string having two `\t`s try: - with open('/proc/cpuinfo') as f: + with open("/proc/cpuinfo") as f: raw_cpu_info = f.read() except Exception as e: cpu_info.status.type = StatusType.FAILED - cpu_info.status.messages.append(f"Could not open /proc/cpuinfo: {str(e)}") + cpu_info.status.messages.append(f"Could not open /proc/cpuinfo: {e!s}") return cpu_info if not raw_cpu_info: @@ -168,7 +166,7 @@ def fetch_cpu_info() -> CPUInfo: cpu_info.status.messages.append("/proc/cpuinfo has no content") return cpu_info - architecture = subprocess.run(['uname', '-m'], capture_output=True, text=True) + architecture = subprocess.run(["uname", "-m"], capture_output=True, text=True) if ("aarch64" in architecture.stdout) or ("arm" in architecture.stdout): return fetch_arm_cpu_info(raw_cpu_info) diff --git a/src/hwprobe/core/linux/display.py b/src/hwprobe/core/linux/display.py index 04d479e..dd93da9 100644 --- a/src/hwprobe/core/linux/display.py +++ b/src/hwprobe/core/linux/display.py @@ -2,7 +2,7 @@ import re from typing import Optional -from hwprobe.core.common.edid import parse_edid, INTERFACE_ENUM +from hwprobe.core.common.edid import INTERFACE_ENUM, parse_edid from hwprobe.core.linux.common import pci_path_linux from hwprobe.models.display_models import DisplayInfo, DisplayModuleInfo from hwprobe.models.status_models import StatusType @@ -10,16 +10,16 @@ _PCI_BDF_PATTERN = re.compile(r"^[0-9a-fA-F]{4}:[0-9a-fA-F]{2}:[0-9a-fA-F]{2}\.[0-7]$") DRM_CONNECTOR_TYPE = { - "eDP": INTERFACE_ENUM[5], # DisplayPort - "DP": INTERFACE_ENUM[5], # DisplayPort + "eDP": INTERFACE_ENUM[5], # DisplayPort + "DP": INTERFACE_ENUM[5], # DisplayPort "HDMI-A": INTERFACE_ENUM[2], # HDMI "HDMI-B": INTERFACE_ENUM[3], # HDMI (B) - "DVI-D": INTERFACE_ENUM[1], # DVI - "DVI-I": INTERFACE_ENUM[1], # DVI - "DVI-A": INTERFACE_ENUM[1], # DVI - "VGA": "Analog", - "LVDS": "LVDS", - "DSI": "DSI", + "DVI-D": INTERFACE_ENUM[1], # DVI + "DVI-I": INTERFACE_ENUM[1], # DVI + "DVI-A": INTERFACE_ENUM[1], # DVI + "VGA": "Analog", + "LVDS": "LVDS", + "DSI": "DSI", } @@ -41,7 +41,8 @@ def _parse_connector_type(device_path: str) -> Optional[str]: def _fetch_individual_monitor_info(device_path: str) -> Optional[DisplayModuleInfo]: edid_path = os.path.join(device_path, "edid") - if not os.path.exists(edid_path): return None + if not os.path.exists(edid_path): + return None parent_path = os.path.join(device_path, "device") # todo: populate parent graphics card info @@ -49,7 +50,8 @@ def _fetch_individual_monitor_info(device_path: str) -> Optional[DisplayModuleIn with open(edid_path, "rb") as f: edid_data = f.read() - if len(edid_data) == 0: return None + if len(edid_data) == 0: + return None monitor_data = parse_edid(edid_data) @@ -91,6 +93,6 @@ def fetch_display_info(): display_info.modules.append(response) except Exception as e: display_info.status.type = StatusType.PARTIAL - display_info.status.messages.append(f"Display Info ({child}): {str(e)}") + display_info.status.messages.append(f"Display Info ({child}): {e!s}") return display_info diff --git a/src/hwprobe/core/linux/dmi_decode.py b/src/hwprobe/core/linux/dmi_decode.py index 5e840e1..b3b6067 100644 --- a/src/hwprobe/core/linux/dmi_decode.py +++ b/src/hwprobe/core/linux/dmi_decode.py @@ -50,5 +50,5 @@ def get_string_entry(string, n): 0x1A: "DDR4", 0x1B: "LPDDR", 0x1C: "LPDDR2", - 0x1D: "LPDDR3" + 0x1D: "LPDDR3", } diff --git a/src/hwprobe/core/linux/graphics.py b/src/hwprobe/core/linux/graphics.py index 2e16d03..8348364 100644 --- a/src/hwprobe/core/linux/graphics.py +++ b/src/hwprobe/core/linux/graphics.py @@ -43,14 +43,7 @@ def _pcie_gen(device) -> Optional[int]: raw_speed = f.read().strip() # e.g., "16.0 GT/s" # Mapping Dictionary - speed_to_gen = { - "2.5 GT/s": 1, - "5.0 GT/s": 2, - "8.0 GT/s": 3, - "16.0 GT/s": 4, - "32.0 GT/s": 5, - "64.0 GT/s": 6 - } + speed_to_gen = {"2.5 GT/s": 1, "5.0 GT/s": 2, "8.0 GT/s": 3, "16.0 GT/s": 4, "32.0 GT/s": 5, "64.0 GT/s": 6} for k, v in speed_to_gen.items(): """ `8.0 GT/s PCIe` may be a possible candidate, so we dont use direct matching""" @@ -59,7 +52,7 @@ def _pcie_gen(device) -> Optional[int]: return None - except Exception as e: + except Exception: return None @@ -87,10 +80,14 @@ def _populate_amd_info(gpu: GPUInfo, device: str) -> GPUInfo: def _populate_nvidia_info(gpu: GPUInfo, device: str) -> GPUInfo: gpu_name, pcie_width, pcie_gen, vram_total = fetch_gpu_details_nvidia(device) - if gpu_name: gpu.name = gpu_name - if pcie_width: gpu.pcie_width = pcie_width - if pcie_gen: gpu.pcie_gen = pcie_gen - if vram_total: gpu.vram = Megabyte(capacity=vram_total) + if gpu_name: + gpu.name = gpu_name + if pcie_width: + gpu.pcie_width = pcie_width + if pcie_gen: + gpu.pcie_gen = pcie_gen + if vram_total: + gpu.vram = Megabyte(capacity=vram_total) return gpu @@ -99,14 +96,14 @@ def _populate_lspci_info(gpu: GPUInfo, device: str) -> GPUInfo: try: lspci_output = subprocess.run(["lspci", "-s", device, "-vmm"], capture_output=True, text=True).stdout # We gather all data here and parse whatever data we have. Subsystem data may not be returned. - except Exception as e: + except Exception: # lspci may not be available in some distros raise data = {} for line in lspci_output.splitlines(): if ":" in line: - key, value = line.split(':', maxsplit=1) + key, value = line.split(":", maxsplit=1) data[key.strip()] = value.strip() gpu.manufacturer = data.get("Vendor") @@ -168,7 +165,7 @@ def fetch_graphics_info() -> GraphicsInfo: gpu.pcie_gen = pcie_gen else: graphics_info.status.type = StatusType.PARTIAL - graphics_info.status.messages.append(f"Could not get PCI gen") + graphics_info.status.messages.append("Could not get PCI gen") if gpu.vendor_id == "0x1002": gpu = _populate_amd_info(gpu, device) diff --git a/src/hwprobe/core/linux/memory.py b/src/hwprobe/core/linux/memory.py index ff36a4c..80ff6b9 100644 --- a/src/hwprobe/core/linux/memory.py +++ b/src/hwprobe/core/linux/memory.py @@ -1,12 +1,11 @@ import os from typing import Optional -from hwprobe.core.linux.dmi_decode import get_string_entry, MEMORY_TYPE -from hwprobe.models.memory_models import MemoryInfo, MemoryModuleSlot, MemoryModuleInfo -from hwprobe.models.size_models import Megabyte, Kilobyte, StorageSize +from hwprobe.core.linux.dmi_decode import MEMORY_TYPE, get_string_entry +from hwprobe.models.memory_models import MemoryInfo, MemoryModuleInfo, MemoryModuleSlot +from hwprobe.models.size_models import Kilobyte, Megabyte, StorageSize from hwprobe.models.status_models import StatusType - # Thank you to [Quist](https://github.com/nadiaholmquist) for helping with our understanding of this. @@ -30,10 +29,7 @@ def _dimm_type(value: bytes) -> Optional[str]: def _dimm_slot(strings: list[bytes], value: bytes) -> Optional[MemoryModuleSlot]: - return MemoryModuleSlot( - channel=get_string_entry(strings, value[0x10]), - bank=get_string_entry(strings, value[0x11]) - ) + return MemoryModuleSlot(channel=get_string_entry(strings, value[0x10]), bank=get_string_entry(strings, value[0x11])) def _dimm_capacity(value: bytes) -> Optional[StorageSize]: @@ -136,7 +132,7 @@ def fetch_memory_info() -> MemoryInfo: try: length_field = value[0x1] - strings = value[length_field:len(value)].split(b'\0') + strings = value[length_field : len(value)].split(b"\0") module.part_number = _part_no(strings, value) diff --git a/src/hwprobe/core/linux/network.py b/src/hwprobe/core/linux/network.py index b5043a6..8c870ce 100644 --- a/src/hwprobe/core/linux/network.py +++ b/src/hwprobe/core/linux/network.py @@ -10,7 +10,8 @@ def _enrich_with_sysfs_info(nic: NICInfo, status: Status) -> None: """Helper to read hardware details directly from Linux sysfs.""" interface_name = nic.interface - if not interface_name: return + if not interface_name: + return # todo: pci.ids file may be locally stored in Linux distros. # When a scraper-parser is made, make use of this, to get device name. diff --git a/src/hwprobe/core/linux/storage.py b/src/hwprobe/core/linux/storage.py index a0f71e6..1f11090 100644 --- a/src/hwprobe/core/linux/storage.py +++ b/src/hwprobe/core/linux/storage.py @@ -1,8 +1,8 @@ import os from hwprobe.models.size_models import Megabyte -from hwprobe.models.status_models import StatusType, Status -from hwprobe.models.storage_models import StorageInfo, DiskInfo +from hwprobe.models.status_models import Status, StatusType +from hwprobe.models.storage_models import DiskInfo, StorageInfo def _fetch_emmc_info(folder: str) -> tuple[DiskInfo, Status]: @@ -48,7 +48,7 @@ def _fetch_emmc_info(folder: str) -> tuple[DiskInfo, Status]: size = open(f"{path}/size").read().strip() size_in_bytes = int(size) * 512 - disk.size = Megabyte(capacity=(size_in_bytes // 1024 ** 2)) + disk.size = Megabyte(capacity=(size_in_bytes // 1024**2)) return disk, status @@ -76,11 +76,7 @@ def _fetch_standard_disk_info(folder: str) -> tuple[DiskInfo, Status]: rotational = open(f"{path}/queue/rotational").read().strip() removable = open(f"{path}/removable").read().strip() - disk.type = ( - "Solid State Drive (SSD)" - if rotational == "0" - else "Hard Disk Drive (HDD)" - ) + disk.type = "Solid State Drive (SSD)" if rotational == "0" else "Hard Disk Drive (HDD)" disk.location = "Internal" if removable == "0" else "External" if "nvme" in folder: @@ -96,7 +92,7 @@ def _fetch_standard_disk_info(folder: str) -> tuple[DiskInfo, Status]: size = open(f"{path}/size").read().strip() size_in_bytes = int(size) * 512 - disk.size = Megabyte(capacity=(size_in_bytes // 1024 ** 2)) + disk.size = Megabyte(capacity=(size_in_bytes // 1024**2)) return disk, status @@ -134,6 +130,6 @@ def fetch_storage_info() -> StorageInfo: except Exception as e: storage_info.status.type = StatusType.PARTIAL - storage_info.status.messages.append(f"Disk Info ({folder}): {str(e)}") + storage_info.status.messages.append(f"Disk Info ({folder}): {e!s}") return storage_info diff --git a/src/hwprobe/core/mac/display.py b/src/hwprobe/core/mac/display.py index 806498a..caffb23 100644 --- a/src/hwprobe/core/mac/display.py +++ b/src/hwprobe/core/mac/display.py @@ -32,7 +32,8 @@ def _enrich_data_from_edid(monitor_info: DisplayModuleInfo, edid_string: str) -> if getattr(monitor_info, field) is None: setattr(monitor_info, field, getattr(data, field)) # Update the resolution as well - if data.resolution is None: return monitor_info + if data.resolution is None: + return monitor_info if monitor_info.resolution is None: monitor_info.resolution = data.resolution return monitor_info diff --git a/src/hwprobe/core/mac/graphics.py b/src/hwprobe/core/mac/graphics.py index 20b0251..d26e8ff 100644 --- a/src/hwprobe/core/mac/graphics.py +++ b/src/hwprobe/core/mac/graphics.py @@ -1,5 +1,4 @@ - -from hwprobe.models.gpu_models import GraphicsInfo, GPUInfo, AppleExtendedGPUInfo +from hwprobe.models.gpu_models import AppleExtendedGPUInfo, GPUInfo, GraphicsInfo from hwprobe.models.size_models import Megabyte from hwprobe.models.status_models import StatusType @@ -22,7 +21,8 @@ def fetch_graphics_info() -> GraphicsInfo: # The binding raises FileNotFoundError at import time if libdevice_info.dylib is missing, # and RuntimeError at call time if the C library returns -1 try: - from hwprobe.interops.mac.bindings.gpu_info import get_gpu_info, GPUProperties + from hwprobe.interops.mac.bindings.gpu_info import GPUProperties, get_gpu_info + gpu_list: list[GPUProperties] = get_gpu_info() except FileNotFoundError as e: @@ -52,9 +52,7 @@ def fetch_graphics_info() -> GraphicsInfo: module.vendor_id = hex(gpu.vendor_id) module.manufacturer = _VENDOR_MAP.get(gpu.vendor_id, "Unknown") else: - graphics_info.status.make_partial( - f"Could not get vendor ID for GPU: {module.name}" - ) + graphics_info.status.make_partial(f"Could not get vendor ID for GPU: {module.name}") # Apple Silicon GPUs report 0x0000 for device_id. Flag it as partial for non-Apple-Silicon GPUs. if gpu.device_id: @@ -98,4 +96,3 @@ def fetch_graphics_info() -> GraphicsInfo: graphics_info.modules.append(module) return graphics_info - diff --git a/src/hwprobe/core/mac/manager.py b/src/hwprobe/core/mac/manager.py index 03457be..e75a3a3 100644 --- a/src/hwprobe/core/mac/manager.py +++ b/src/hwprobe/core/mac/manager.py @@ -6,9 +6,7 @@ from hwprobe.core.mac.storage import fetch_storage_info from hwprobe.models.cpu_models import CPUInfo from hwprobe.models.gpu_models import GraphicsInfo -from hwprobe.models.info_models import HardwareInfo -from hwprobe.models.info_models import HardwareManagerInterface -from hwprobe.models.info_models import MacHardwareInfo +from hwprobe.models.info_models import HardwareInfo, HardwareManagerInterface, MacHardwareInfo from hwprobe.models.memory_models import MemoryInfo from hwprobe.models.network_models import NetworkInfo from hwprobe.models.storage_models import StorageInfo diff --git a/src/hwprobe/core/mac/memory.py b/src/hwprobe/core/mac/memory.py index 123c7a6..5a2d9fc 100644 --- a/src/hwprobe/core/mac/memory.py +++ b/src/hwprobe/core/mac/memory.py @@ -2,7 +2,7 @@ import subprocess from hwprobe.models.memory_models import MemoryInfo, MemoryModuleInfo, MemoryModuleSlot -from hwprobe.models.size_models import Megabyte, StorageSize, Gigabyte +from hwprobe.models.size_models import Gigabyte, Megabyte, StorageSize from hwprobe.models.status_models import StatusType @@ -161,25 +161,25 @@ def fetch_memory_info() -> MemoryInfo: dimm_sizes = get_ram_size_from_reg(v) if "manufacturer" in k.lower(): - dimm_manufacturer.extend([x.decode() for x in v.split(b'\x00') if x.decode().strip()]) + dimm_manufacturer.extend([x.decode() for x in v.split(b"\x00") if x.decode().strip()]) if "part-number" in k.lower(): - dimm_part_numbers.extend([x.decode() for x in v.split(b'\x00') if x.decode().strip()]) + dimm_part_numbers.extend([x.decode() for x in v.split(b"\x00") if x.decode().strip()]) if "serial-number" in k.lower(): - dimm_serial_number.extend([x.decode() for x in v.split(b'\x00') if x.decode().strip()]) + dimm_serial_number.extend([x.decode() for x in v.split(b"\x00") if x.decode().strip()]) if "speed" in k.lower(): - dimm_speeds.extend([x.decode() for x in v.split(b'\x00') if x.decode().strip()]) + dimm_speeds.extend([x.decode() for x in v.split(b"\x00") if x.decode().strip()]) if "type" in k.lower(): - dimm_types.extend([x.decode() for x in v.split(b'\x00') if x.decode().strip()]) + dimm_types.extend([x.decode() for x in v.split(b"\x00") if x.decode().strip()]) if "ecc-enabled" in k.lower(): ecc_enabled = ecc_enabled or v if "slot-name" in k.lower(): - dimm_slots = [x.decode().split("/") for x in v.split(b'\x00') if x.decode().strip()] + dimm_slots = [x.decode().split("/") for x in v.split(b"\x00") if x.decode().strip()] # Now we attempt to get more accurate RAM Module Capacities """ @@ -202,13 +202,20 @@ def fetch_memory_info() -> MemoryInfo: "Failed to get RAM size from system profiler. RAM Capacity may not be accurate: " + str(e) ) - except Exception as e: memory_info.status.type = StatusType.PARTIAL memory_info.status.messages.append("Error parsing ioreg plist: " + str(e)) - n_modules = max([len(dimm_manufacturer), len(dimm_part_numbers), len(dimm_serial_number), - len(dimm_speeds), len(dimm_types), len(dimm_slots)]) + n_modules = max( + [ + len(dimm_manufacturer), + len(dimm_part_numbers), + len(dimm_serial_number), + len(dimm_speeds), + len(dimm_types), + len(dimm_slots), + ] + ) for i in range(n_modules): module = MemoryModuleInfo() @@ -222,10 +229,7 @@ def fetch_memory_info() -> MemoryInfo: if i < len(dimm_sizes): module.capacity = dimm_sizes[i] if i < len(dimm_slots): - module.slot = MemoryModuleSlot( - channel=dimm_slots[i][0], - bank=dimm_slots[i][1] - ) + module.slot = MemoryModuleSlot(channel=dimm_slots[i][0], bank=dimm_slots[i][1]) if i < len(dimm_speeds): module.frequency_mhz = int(dimm_speeds[i].removesuffix("MHz")) module.supports_ecc = ecc_enabled diff --git a/src/hwprobe/core/mac/network.py b/src/hwprobe/core/mac/network.py index c4a3452..bf44948 100644 --- a/src/hwprobe/core/mac/network.py +++ b/src/hwprobe/core/mac/network.py @@ -89,10 +89,7 @@ def _get_bsd_interface_apple_silicon(item: dict, driver: str = "AppleBCMWLANCore if driver in mapping: return _traverse_ioreg(item, mapping[driver]) - return ( - _traverse_ioreg(item, _STEPS_BCM_WLAN) - or _traverse_ioreg(item, _STEPS_WLAN_DRIVER) - ) + return _traverse_ioreg(item, _STEPS_BCM_WLAN) or _traverse_ioreg(item, _STEPS_WLAN_DRIVER) def _fetch_airport_details() -> dict[str, NICInfo]: @@ -110,7 +107,8 @@ def _fetch_airport_details() -> dict[str, NICInfo]: for item in plist: driver = item.get("IORegistryEntryName") - if not driver: continue + if not driver: + continue if driver == "AirPort_BrcmNIC": # Intel Macs, usually @@ -122,7 +120,8 @@ def _fetch_airport_details() -> dict[str, NICInfo]: nic_info = NICInfo() nic_info.vendor_id = "0x" + vendor nic_info.device_id = "0x" + device - if io_model: nic_info.name = io_model + if io_model: + nic_info.name = io_model for child in item.get("IORegistryEntryChildren", []): if not child.get("IOObjectClass", "") == "AirPort_BrcmNIC_Interface": @@ -161,7 +160,8 @@ def _fetch_airport_details() -> dict[str, NICInfo]: else: nic_info.manufacturer = "Apple" - if chipset: nic_info.name = f"Wi-Fi ({chipset} chipset)" + if chipset: + nic_info.name = f"Wi-Fi ({chipset} chipset)" res[bsd_identifier] = nic_info @@ -208,7 +208,8 @@ def _fetch_system_profiler_details(valid_bsd_interfaces: list[str]) -> NetworkIn module = NICInfo() bsd_interface_name = network_controller.get("interface") - if bsd_interface_name not in valid_bsd_interfaces: continue + if bsd_interface_name not in valid_bsd_interfaces: + continue module.interface = bsd_interface_name module.name = network_controller.get("_name", "") @@ -224,15 +225,17 @@ def _fetch_system_profiler_details(valid_bsd_interfaces: list[str]) -> NetworkIn if ip_addresses: module.ip_address = ip_addresses[0] - if module.type == 'Ethernet': - if ethernet_info is None: ethernet_info = _fetch_ethernet_details() + if module.type == "Ethernet": + if ethernet_info is None: + ethernet_info = _fetch_ethernet_details() if bsd_interface_name in ethernet_info: module.vendor_id = ethernet_info[bsd_interface_name].vendor_id module.manufacturer = ethernet_info[bsd_interface_name].manufacturer module.device_id = ethernet_info[bsd_interface_name].device_id - elif module.type == 'AirPort': - if airport_info is None: airport_info = _fetch_airport_details() + elif module.type == "AirPort": + if airport_info is None: + airport_info = _fetch_airport_details() if bsd_interface_name in airport_info: if manufacturer := airport_info[bsd_interface_name].manufacturer: module.manufacturer = manufacturer diff --git a/src/hwprobe/core/mac/storage.py b/src/hwprobe/core/mac/storage.py index ecff035..7733a55 100644 --- a/src/hwprobe/core/mac/storage.py +++ b/src/hwprobe/core/mac/storage.py @@ -1,7 +1,6 @@ - from hwprobe.models.size_models import Megabyte from hwprobe.models.status_models import StatusType -from hwprobe.models.storage_models import StorageInfo, DiskInfo +from hwprobe.models.storage_models import DiskInfo, StorageInfo STORAGE_MAP = { "Solid State": "Solid State Drive (SSD)", @@ -18,7 +17,8 @@ def fetch_storage_info() -> StorageInfo: storage_info = StorageInfo() try: - from hwprobe.interops.mac.bindings.storage_info import get_storage_info, StorageDeviceProperties + from hwprobe.interops.mac.bindings.storage_info import StorageDeviceProperties, get_storage_info + disk_list: list[StorageDeviceProperties] = get_storage_info() except FileNotFoundError as e: diff --git a/src/hwprobe/core/windows/audio.py b/src/hwprobe/core/windows/audio.py index 628af29..ac06ac1 100644 --- a/src/hwprobe/core/windows/audio.py +++ b/src/hwprobe/core/windows/audio.py @@ -23,9 +23,7 @@ def fetch_audio_info_fast() -> AudioInfo: # the method couldn't execute successfully if res != STATUS_OK: audio_info.status.type = StatusType.FAILED - audio_info.status.messages.append( - f"Audio HW info query failed with status code: {res}" - ) + audio_info.status.messages.append(f"Audio HW info query failed with status code: {res}") return audio_info decoded = raw_data.value.decode("utf-8", errors="ignore").strip() diff --git a/src/hwprobe/core/windows/baseboard.py b/src/hwprobe/core/windows/baseboard.py index 26f49ad..a621a9a 100644 --- a/src/hwprobe/core/windows/baseboard.py +++ b/src/hwprobe/core/windows/baseboard.py @@ -19,10 +19,10 @@ def fetch_baseboard_info() -> BaseboardInfo: baseboard_info.status.messages.append("Failed to fetch SMBIOS hardware info for Baseboard") return baseboard_info - manufacturer = info.motherboardManufacturer.decode(errors='ignore').rstrip('\x00') - model = info.motherboardModel.decode(errors='ignore').rstrip('\x00') - chassis_type = info.chassisType.decode(errors='ignore').rstrip('\x00') - cpu_socket = info.cpuSocket.decode(errors='ignore').rstrip('\x00') + manufacturer = info.motherboardManufacturer.decode(errors="ignore").rstrip("\x00") + model = info.motherboardModel.decode(errors="ignore").rstrip("\x00") + chassis_type = info.chassisType.decode(errors="ignore").rstrip("\x00") + cpu_socket = info.cpuSocket.decode(errors="ignore").rstrip("\x00") baseboard_info.manufacturer = manufacturer if manufacturer else None baseboard_info.model = model if model else None diff --git a/src/hwprobe/core/windows/common.py b/src/hwprobe/core/windows/common.py index 6fe27c8..f5f795b 100644 --- a/src/hwprobe/core/windows/common.py +++ b/src/hwprobe/core/windows/common.py @@ -37,8 +37,6 @@ def format_pci_path(raw_path: str) -> str: device = full_val >> 8 function = full_val & 0xFF prefix = pci_match.group(1) - formatted_parts.append( - f"{prefix[0].upper() + prefix[1:].lower()}(0x{device:X},0x{function:X})" - ) + formatted_parts.append(f"{prefix[0].upper() + prefix[1:].lower()}(0x{device:X},0x{function:X})") return "/".join(formatted_parts) diff --git a/src/hwprobe/core/windows/cpu.py b/src/hwprobe/core/windows/cpu.py index 6679bc5..ce7e43e 100644 --- a/src/hwprobe/core/windows/cpu.py +++ b/src/hwprobe/core/windows/cpu.py @@ -25,13 +25,13 @@ def is_processor_feature_present(feature_id: int) -> bool: def get_arm_version() -> str: """ We use instructions that were introduced in different ARM versions to determine the ARM version. - + Introduced in ARMv9: - SVE2 - FEAT_SSVE_FP8DOT2 (78), FEAT_SSVE_FP8DOT4 (79), and FEAT_SSVE_FP8FMA (80) - + Introduced in ARMv8: - Full AArch64 Instructions - FEAT_SME_FA64 (88) - + Otherwise - we can assume it's ARMv7 or lower. """ @@ -47,7 +47,7 @@ def get_features() -> list[str]: """ We use the Win32 API function IsProcessorFeaturePresent to check for SSE features. https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-isprocessorfeaturepresent - + Feature IDs: - SSE - 6 - SSE2 - 10 @@ -69,12 +69,7 @@ def parse_registry(): model_key = "ProcessorNameString" vendor_key = "VendorIdentifier" - with winreg.OpenKey( - winreg.HKEY_LOCAL_MACHINE, - key_path, - 0, - winreg.KEY_READ - ) as key: + with winreg.OpenKey(winreg.HKEY_LOCAL_MACHINE, key_path, 0, winreg.KEY_READ) as key: model_name, _ = winreg.QueryValueEx(key, model_key) vendor, _ = winreg.QueryValueEx(key, vendor_key) return model_name, vendor @@ -124,14 +119,9 @@ class SYSTEM_LOGICAL_PROCESSOR_INFORMATION(ctypes.Structure): count = buffer_size.value // ctypes.sizeof(SYSTEM_LOGICAL_PROCESSOR_INFORMATION) buffer = (SYSTEM_LOGICAL_PROCESSOR_INFORMATION * count)() - ctypes.windll.kernel32.GetLogicalProcessorInformation( - buffer, - ctypes.byref(buffer_size) - ) + ctypes.windll.kernel32.GetLogicalProcessorInformation(buffer, ctypes.byref(buffer_size)) - physical_cores = sum( - 1 for info in buffer if info.Relationship == RelationProcessorCore - ) + physical_cores = sum(1 for info in buffer if info.Relationship == RelationProcessorCore) return physical_cores diff --git a/src/hwprobe/core/windows/display.py b/src/hwprobe/core/windows/display.py index e10a14d..368e8df 100644 --- a/src/hwprobe/core/windows/display.py +++ b/src/hwprobe/core/windows/display.py @@ -16,43 +16,44 @@ from typing import Optional from hwprobe.core.windows.win_enum import DISPLAY_CON_TYPE + # todo: refactor to new bindings from hwprobe.interops.win.legacy.constants import ( - STATUS_OK, - GUID_DEVINTERFACE_MONITOR, - DIGCF_PRESENT, - DIGCF_DEVICEINTERFACE, DICS_FLAG_GLOBAL, + DIGCF_DEVICEINTERFACE, + DIGCF_PRESENT, DIREG_DEV, - KEY_READ, - ENUM_CURRENT_SETTINGS, - DMDO_DEFAULT, DMDO_90, DMDO_180, DMDO_270, + DMDO_DEFAULT, + ENUM_CURRENT_SETTINGS, + GUID_DEVINTERFACE_MONITOR, + KEY_READ, + STATUS_OK, ) from hwprobe.interops.win.legacy.signatures import ( + EnumDisplayDevicesA, + EnumDisplayMonitors, + EnumDisplaySettingsA, + GetDisplayPathInfo, GetGPUForDisplay, - SetupDiGetClassDevsA, + GetMonitorInfoA, + RegCloseKey, + RegQueryValueExA, + SetupDiDestroyDeviceInfoList, SetupDiEnumDeviceInterfaces, + SetupDiGetClassDevsA, SetupDiGetDeviceInterfaceDetailA, SetupDiOpenDevRegKey, - RegQueryValueExA, - RegCloseKey, - SetupDiDestroyDeviceInfoList, - GetMonitorInfoA, - EnumDisplaySettingsA, - EnumDisplayDevicesA, - GetDisplayPathInfo, - EnumDisplayMonitors, ) from hwprobe.interops.win.legacy.structs import ( - SP_DEVICE_INTERFACE_DATA, - SP_DEVINFO_DATA, - MONITORINFOEXA, DEVMODEA, DISPLAY_DEVICEA, MONITORENUMPROC, + MONITORINFOEXA, + SP_DEVICE_INTERFACE_DATA, + SP_DEVINFO_DATA, ) from hwprobe.models.display_models import DisplayInfo, DisplayModuleInfo from hwprobe.models.status_models import Status, StatusType @@ -213,9 +214,9 @@ def _decode_manufacturer_code(vendor_id: int) -> str: Returns: Three-letter manufacturer code (e.g., "SAM" for Samsung, "DEL" for Dell) """ - char1 = chr(((vendor_id >> 10) & 0x1F) + ord('A') - 1) - char2 = chr(((vendor_id >> 5) & 0x1F) + ord('A') - 1) - char3 = chr((vendor_id & 0x1F) + ord('A') - 1) + char1 = chr(((vendor_id >> 10) & 0x1F) + ord("A") - 1) + char2 = chr(((vendor_id >> 5) & 0x1F) + ord("A") - 1) + char3 = chr((vendor_id & 0x1F) + ord("A") - 1) return f"{char1}{char2}{char3}" @@ -233,7 +234,7 @@ def _calculate_diagonal_inches(width_cm: int, height_cm: int) -> float: if width_cm <= 0 or height_cm <= 0: return 0.0 - diagonal_cm = (width_cm ** 2 + height_cm ** 2) ** 0.5 + diagonal_cm = (width_cm**2 + height_cm**2) ** 0.5 return round(diagonal_cm / 2.54) @@ -262,8 +263,8 @@ def parse_edid(edid: bytes) -> Optional[dict]: return None # Parse vendor and product IDs - vendor_id = struct.unpack(">H", edid[_EDID_VENDOR_OFFSET:_EDID_VENDOR_OFFSET + 2])[0] - product_id = struct.unpack("H", edid[_EDID_VENDOR_OFFSET : _EDID_VENDOR_OFFSET + 2])[0] + product_id = struct.unpack(" Optional[dict]: # First descriptor (at 0x36) is reserved for Preferred Timing Mode, so we start at 0x48 for i in range(_EDID_DESCRIPTOR_COUNT): offset = _EDID_DESCRIPTOR_BASE_OFFSET + (i * _EDID_DESCRIPTOR_SIZE) - descriptor = edid[offset:offset + _EDID_DESCRIPTOR_SIZE] + descriptor = edid[offset : offset + _EDID_DESCRIPTOR_SIZE] if serial is None: serial = _extract_descriptor_text(descriptor, _EDID_SERIAL_MARKER) @@ -431,11 +432,11 @@ def _enumerate_and_find_edid(device_info_set, hwid_upper: str) -> Optional[dict] # Enumerate next interface if not SetupDiEnumDeviceInterfaces( - device_info_set, - None, - ctypes.byref(GUID_DEVINTERFACE_MONITOR), - interface_index, - ctypes.byref(interface_data), + device_info_set, + None, + ctypes.byref(GUID_DEVINTERFACE_MONITOR), + interface_index, + ctypes.byref(interface_data), ): break @@ -483,12 +484,12 @@ def _try_get_edid_for_interface(device_info_set, interface_data, hwid_upper: str # Get interface detail and device info if not SetupDiGetDeviceInterfaceDetailA( - device_info_set, - ctypes.byref(interface_data), - detail_buffer, - required_size, - None, - ctypes.byref(device_data), + device_info_set, + ctypes.byref(interface_data), + detail_buffer, + required_size, + None, + ctypes.byref(device_data), ): return None @@ -546,10 +547,7 @@ def _get_connection_type(connector_info: Optional[dict]) -> Optional[str]: return None -def _fetch_edid_for_monitor( - connector_info: Optional[dict], - pnp_device_id: str -) -> tuple[Optional[dict], Optional[str]]: +def _fetch_edid_for_monitor(connector_info: Optional[dict], pnp_device_id: str) -> tuple[Optional[dict], Optional[str]]: """ Fetch EDID data for a monitor, preferring display path over PNP ID. @@ -581,13 +579,13 @@ def _fetch_edid_for_monitor( def _build_monitor_info( - device_id: str, - hardware_id: str, - device_path: Optional[str], - display_mode: DEVMODEA, - edid: Optional[dict], - gpu_name: Optional[str], - connection_type: Optional[str], + device_id: str, + hardware_id: str, + device_path: Optional[str], + display_mode: DEVMODEA, + edid: Optional[dict], + gpu_name: Optional[str], + connection_type: Optional[str], ) -> DisplayModuleInfo: """ Construct a DisplayModuleInfo object from collected data. @@ -617,10 +615,7 @@ def _build_monitor_info( monitor.resolution.width = display_mode.dmPelsWidth monitor.resolution.height = display_mode.dmPelsHeight monitor.resolution.refresh_rate = display_mode.dmDisplayFrequency - monitor.resolution.aspect_ratio = get_aspect_ratio( - display_mode.dmPelsWidth, - display_mode.dmPelsHeight - ) + monitor.resolution.aspect_ratio = get_aspect_ratio(display_mode.dmPelsWidth, display_mode.dmPelsHeight) # Orientation monitor.orientation = _get_orientation_name(display_mode.dmDisplayOrientation) diff --git a/src/hwprobe/core/windows/graphics.py b/src/hwprobe/core/windows/graphics.py index 301d936..c1c57f6 100644 --- a/src/hwprobe/core/windows/graphics.py +++ b/src/hwprobe/core/windows/graphics.py @@ -1,4 +1,4 @@ -from hwprobe.interops.win.bindings.gpu_info import get_gpu_info, GPUProperties +from hwprobe.interops.win.bindings.gpu_info import GPUProperties, get_gpu_info from hwprobe.models.gpu_models import GPUInfo, GraphicsInfo from hwprobe.models.size_models import Megabyte from hwprobe.models.status_models import StatusType diff --git a/src/hwprobe/core/windows/manager.py b/src/hwprobe/core/windows/manager.py index c360c8b..a7fb982 100644 --- a/src/hwprobe/core/windows/manager.py +++ b/src/hwprobe/core/windows/manager.py @@ -9,9 +9,7 @@ from hwprobe.models.cpu_models import CPUInfo from hwprobe.models.display_models import DisplayInfo from hwprobe.models.gpu_models import GraphicsInfo -from hwprobe.models.info_models import HardwareInfo -from hwprobe.models.info_models import HardwareManagerInterface -from hwprobe.models.info_models import WindowsHardwareInfo +from hwprobe.models.info_models import HardwareInfo, HardwareManagerInterface, WindowsHardwareInfo from hwprobe.models.memory_models import MemoryInfo from hwprobe.models.network_models import NetworkInfo from hwprobe.models.storage_models import StorageInfo diff --git a/src/hwprobe/core/windows/memory.py b/src/hwprobe/core/windows/memory.py index cebe1f8..536dee0 100644 --- a/src/hwprobe/core/windows/memory.py +++ b/src/hwprobe/core/windows/memory.py @@ -1,6 +1,7 @@ import ctypes from hwprobe.core.windows.win_enum import ECC_MEMORY_TYPE, MEMORY_TYPE + # todo: refactor to new bindings from hwprobe.interops.win.legacy.constants import ECC_MULTI_BIT, ECC_SINGLE_BIT from hwprobe.interops.win.legacy.signatures import GetWmiInfo @@ -41,11 +42,7 @@ def check_ecc() -> tuple[bool, str]: return False, "Unknown" first_line = raw_data.split("\n")[0] - parsed_data = { - x.split("=", 1)[0]: x.split("=", 1)[1] - for x in first_line.split("|") - if "=" in x - } + parsed_data = {x.split("=", 1)[0]: x.split("=", 1)[1] for x in first_line.split("|") if "=" in x} ecc_type = parsed_data.get("MemoryErrorCorrection", "Unknown") supported = False @@ -55,9 +52,7 @@ def check_ecc() -> tuple[bool, str]: if ecc_type == ECC_SINGLE_BIT or ecc_type == ECC_MULTI_BIT: supported = True - return supported, ( - ECC_MEMORY_TYPE[ecc_type] if ecc_type in ECC_MEMORY_TYPE else "Unknown" - ) + return supported, (ECC_MEMORY_TYPE[ecc_type] if ecc_type in ECC_MEMORY_TYPE else "Unknown") def fetch_wmi_memory_info() -> MemoryInfo: @@ -98,9 +93,7 @@ def fetch_wmi_memory_info() -> MemoryInfo: module = MemoryModuleInfo() unparsed = line.split("|") - parsed_data = { - x.split("=", 1)[0]: x.split("=", 1)[1] for x in unparsed if "=" in x - } + parsed_data = {x.split("=", 1)[0]: x.split("=", 1)[1] for x in unparsed if "=" in x} bank_label = parsed_data["BankLabel"] capacity = parsed_data["Capacity"] diff --git a/src/hwprobe/core/windows/network.py b/src/hwprobe/core/windows/network.py index 71eb2a6..bb3312a 100644 --- a/src/hwprobe/core/windows/network.py +++ b/src/hwprobe/core/windows/network.py @@ -1,10 +1,11 @@ import ctypes from hwprobe.core.windows.common import format_acpi_path, format_pci_path + # todo: refactor to new bindings from hwprobe.interops.win.legacy.constants import STATUS_OK from hwprobe.interops.win.legacy.signatures import GetNetworkHardwareInfo -from hwprobe.models.network_models import NICInfo, NetworkInfo +from hwprobe.models.network_models import NetworkInfo, NICInfo from hwprobe.models.status_models import Status, StatusType from hwprobe.util.location_paths import get_location_paths @@ -21,9 +22,7 @@ def fetch_network_info_fast() -> NetworkInfo: # the method couldn't execute successfully if res != STATUS_OK: network_info.status.type = StatusType.FAILED - network_info.status.messages.append( - f"Network HW info query failed with status code: {res}" - ) + network_info.status.messages.append(f"Network HW info query failed with status code: {res}") return network_info decoded = raw_data.value.decode("utf-8", errors="ignore").strip() @@ -47,9 +46,7 @@ def fetch_network_info_fast() -> NetworkInfo: if not pnp_device_id or not manufacturer or not name: network_info.status.type = StatusType.PARTIAL - network_info.status.messages.append( - "Missing PNPDeviceID for network interface; skipping" - ) + network_info.status.messages.append("Missing PNPDeviceID for network interface; skipping") continue if "VEN_" in pnp_device_id and "DEV_" in pnp_device_id: @@ -60,9 +57,7 @@ def fetch_network_info_fast() -> NetworkInfo: module.device_id = pnp_device_id.split("PID_")[1][:4] else: network_info.status.type = StatusType.PARTIAL - network_info.status.messages.append( - f"Could not parse Vendor/Device ID from PNPDeviceID: {pnp_device_id}" - ) + network_info.status.messages.append(f"Could not parse Vendor/Device ID from PNPDeviceID: {pnp_device_id}") loc = get_location_paths(pnp_device_id) diff --git a/src/hwprobe/core/windows/storage.py b/src/hwprobe/core/windows/storage.py index 4b2dfe2..69e52c5 100644 --- a/src/hwprobe/core/windows/storage.py +++ b/src/hwprobe/core/windows/storage.py @@ -1,11 +1,12 @@ import ctypes -from hwprobe.core.windows.win_enum import MEDIA_TYPE, BUS_TYPE +from hwprobe.core.windows.win_enum import BUS_TYPE, MEDIA_TYPE + # todo: refactor to new bindings from hwprobe.interops.win.legacy.signatures import GetWmiInfo from hwprobe.models.size_models import Megabyte from hwprobe.models.status_models import StatusType -from hwprobe.models.storage_models import StorageInfo, DiskInfo +from hwprobe.models.storage_models import DiskInfo, StorageInfo def fetch_wmi_storage_info() -> StorageInfo: @@ -19,10 +20,7 @@ def fetch_wmi_storage_info() -> StorageInfo: buf_size = 256 * 6 * 10 buffer = ctypes.create_string_buffer(buf_size) - query = ( - b"SELECT FriendlyName, MediaType, BusType, Size, Manufacturer, Model FROM " - b"MSFT_PhysicalDisk" - ) + query = b"SELECT FriendlyName, MediaType, BusType, Size, Manufacturer, Model FROM MSFT_PhysicalDisk" GetWmiInfo(query, b"ROOT\\Microsoft\\Windows\\Storage", buffer, buf_size) @@ -37,9 +35,7 @@ def fetch_wmi_storage_info() -> StorageInfo: continue disk = DiskInfo() - props = { - x.split("=", 1)[0]: x.split("=", 1)[1] for x in line.split("|") if "=" in x - } + props = {x.split("=", 1)[0]: x.split("=", 1)[1] for x in line.split("|") if "=" in x} friendly_name = props.get("FriendlyName") media_type = props.get("MediaType") @@ -48,20 +44,10 @@ def fetch_wmi_storage_info() -> StorageInfo: manufacturer = props.get("Manufacturer") model = props.get("Model") - disk.model = ( - model.strip() if model else friendly_name.strip() if friendly_name else None - ) + disk.model = model.strip() if model else friendly_name.strip() if friendly_name else None disk.manufacturer = manufacturer.strip() if manufacturer else None - disk.type = ( - MEDIA_TYPE.get(int(media_type), "Unknown") - if media_type and media_type.isdigit() - else "Unknown" - ) - disk.size = ( - Megabyte(capacity=int(size) // (1024 * 1024)) - if size and size.isdigit() - else None - ) + disk.type = MEDIA_TYPE.get(int(media_type), "Unknown") if media_type and media_type.isdigit() else "Unknown" + disk.size = Megabyte(capacity=int(size) // (1024 * 1024)) if size and size.isdigit() else None # Map bus type conn_type, location = None, None diff --git a/src/hwprobe/core/windows/win_enum.py b/src/hwprobe/core/windows/win_enum.py index c3150d1..2714f9a 100644 --- a/src/hwprobe/core/windows/win_enum.py +++ b/src/hwprobe/core/windows/win_enum.py @@ -45,7 +45,7 @@ 4: "Parity", 5: "Single-bit ECC", 6: "Multi-bit ECC", - 7: "CRC" + 7: "CRC", } MEMORY_TYPE = { diff --git a/src/hwprobe/interops/mac/bindings/gpu_info.py b/src/hwprobe/interops/mac/bindings/gpu_info.py index 058e611..2c5759e 100644 --- a/src/hwprobe/interops/mac/bindings/gpu_info.py +++ b/src/hwprobe/interops/mac/bindings/gpu_info.py @@ -21,8 +21,7 @@ if not _LIB_PATH.exists(): raise FileNotFoundError( - f"libdevice_info.dylib not found at {_LIB_PATH}.\n" - "Build the project first: cmake --build cmake-build-debug" + f"libdevice_info.dylib not found at {_LIB_PATH}.\nBuild the project first: cmake --build cmake-build-debug" ) _lib = ctypes.CDLL(str(_LIB_PATH)) @@ -30,6 +29,7 @@ # ── mirror the C structs ───────────────────────────────────────────────────── + class _AppleGPUProperties(ctypes.Structure): _fields_ = [ ("core_count", ctypes.c_int), @@ -59,6 +59,7 @@ class _GPUProperties(ctypes.Structure): # ── Python-facing dataclasses ──────────────────────────────────────────────── + @dataclass class AppleGPUProperties: core_count: int @@ -131,16 +132,18 @@ def get_gpu_info() -> list[GPUProperties]: acpi = raw.acpi_path.decode("utf-8", errors="replace").strip("\x00") or None pci = raw.pci_path.decode("utf-8", errors="replace").strip("\x00") or None - result.append(GPUProperties( - name=raw.name.decode("utf-8", errors="replace"), - vendor_id=raw.vendor_id, - device_id=raw.device_id, - is_apple_silicon=bool(raw.is_apple_silicon), - apple_gpu=apple, - acpi_path=acpi, - pci_path=pci, - vram_mb=raw.vram_mb, - )) + result.append( + GPUProperties( + name=raw.name.decode("utf-8", errors="replace"), + vendor_id=raw.vendor_id, + device_id=raw.device_id, + is_apple_silicon=bool(raw.is_apple_silicon), + apple_gpu=apple, + acpi_path=acpi, + pci_path=pci, + vram_mb=raw.vram_mb, + ) + ) return result diff --git a/src/hwprobe/interops/mac/bindings/storage_info.py b/src/hwprobe/interops/mac/bindings/storage_info.py index 19d540a..e9cd47b 100644 --- a/src/hwprobe/interops/mac/bindings/storage_info.py +++ b/src/hwprobe/interops/mac/bindings/storage_info.py @@ -19,8 +19,7 @@ if not _LIB_PATH.exists(): raise FileNotFoundError( - f"libdevice_info.dylib not found at {_LIB_PATH}.\n" - "Build the project first: cmake --build cmake-build-debug" + f"libdevice_info.dylib not found at {_LIB_PATH}.\nBuild the project first: cmake --build cmake-build-debug" ) _lib = ctypes.CDLL(str(_LIB_PATH)) @@ -79,15 +78,17 @@ def get_storage_info() -> list[StorageDeviceProperties]: result = [] for i in range(count): raw = buf[i] - result.append(StorageDeviceProperties( - product_name=raw.product_name.decode("utf-8", errors="replace").strip("\x00"), - vendor_name=raw.vendor_name.decode("utf-8", errors="replace").strip("\x00"), - medium_type=raw.medium_type.decode("utf-8", errors="replace").strip("\x00"), - interconnect=raw.interconnect.decode("utf-8", errors="replace").strip("\x00"), - location=raw.location.decode("utf-8", errors="replace").strip("\x00"), - bsd_name=raw.bsd_name.decode("utf-8", errors="replace").strip("\x00"), - size_bytes=raw.size_bytes, - )) + result.append( + StorageDeviceProperties( + product_name=raw.product_name.decode("utf-8", errors="replace").strip("\x00"), + vendor_name=raw.vendor_name.decode("utf-8", errors="replace").strip("\x00"), + medium_type=raw.medium_type.decode("utf-8", errors="replace").strip("\x00"), + interconnect=raw.interconnect.decode("utf-8", errors="replace").strip("\x00"), + location=raw.location.decode("utf-8", errors="replace").strip("\x00"), + bsd_name=raw.bsd_name.decode("utf-8", errors="replace").strip("\x00"), + size_bytes=raw.size_bytes, + ) + ) return result diff --git a/src/hwprobe/interops/win/bindings/gpu_info.py b/src/hwprobe/interops/win/bindings/gpu_info.py index 60e69a0..6bafd28 100644 --- a/src/hwprobe/interops/win/bindings/gpu_info.py +++ b/src/hwprobe/interops/win/bindings/gpu_info.py @@ -20,8 +20,7 @@ if not _LIB_PATH.exists(): raise FileNotFoundError( - f"device_info.dll not found at {_LIB_PATH}.\n" - "Build the project first: cmake --build build --config Release" + f"device_info.dll not found at {_LIB_PATH}.\nBuild the project first: cmake --build build --config Release" ) _lib = ctypes.WinDLL(str(_LIB_PATH)) @@ -29,6 +28,7 @@ # ---- Mirror the C structs ---- + class _WinGPUProperties(ctypes.Structure): _fields_ = [ ("name", ctypes.c_char * 256), @@ -51,6 +51,7 @@ class _WinGPUProperties(ctypes.Structure): # ---- Python-facing dataclass ---- + @dataclass class GPUProperties: name: str @@ -104,19 +105,21 @@ def get_gpu_info() -> list[GPUProperties]: acpi = raw.acpi_path.decode("utf-8", errors="replace").strip("\x00") or None pci = raw.pci_path.decode("utf-8", errors="replace").strip("\x00") or None - result.append(GPUProperties( - name=raw.name.decode("utf-8", errors="replace").strip("\x00"), - manufacturer=raw.manufacturer.decode("utf-8", errors="replace").strip("\x00"), - vendor_id=raw.vendor_id, - device_id=raw.device_id, - subsystem_vendor_id=raw.subsystem_vendor_id, - subsystem_device_id=raw.subsystem_device_id, - acpi_path=acpi, - pci_path=pci, - vram_mb=raw.vram_mb, - pcie_gen=raw.pcie_gen, - pcie_width=raw.pcie_width, - )) + result.append( + GPUProperties( + name=raw.name.decode("utf-8", errors="replace").strip("\x00"), + manufacturer=raw.manufacturer.decode("utf-8", errors="replace").strip("\x00"), + vendor_id=raw.vendor_id, + device_id=raw.device_id, + subsystem_vendor_id=raw.subsystem_vendor_id, + subsystem_device_id=raw.subsystem_device_id, + acpi_path=acpi, + pci_path=pci, + vram_mb=raw.vram_mb, + pcie_gen=raw.pcie_gen, + pcie_width=raw.pcie_width, + ) + ) return result diff --git a/src/hwprobe/models/audio_models.py b/src/hwprobe/models/audio_models.py index c94af4a..31b56e5 100644 --- a/src/hwprobe/models/audio_models.py +++ b/src/hwprobe/models/audio_models.py @@ -1,8 +1,9 @@ from typing import Optional -from hwprobe.models.component_model import ComponentInfo, BaseModel from pydantic import Field +from hwprobe.models.component_model import BaseModel, ComponentInfo + # Also known as an audio endpoint class AudioDeviceInfo(BaseModel): diff --git a/src/hwprobe/models/component_model.py b/src/hwprobe/models/component_model.py index aaafbb8..af283d3 100644 --- a/src/hwprobe/models/component_model.py +++ b/src/hwprobe/models/component_model.py @@ -1,6 +1,7 @@ -from hwprobe.models.status_models import Status from pydantic import BaseModel, Field +from hwprobe.models.status_models import Status + class ComponentInfo(BaseModel): # Each component gets its own fresh status object diff --git a/src/hwprobe/models/cpu_models.py b/src/hwprobe/models/cpu_models.py index e7676d9..c12bcd1 100644 --- a/src/hwprobe/models/cpu_models.py +++ b/src/hwprobe/models/cpu_models.py @@ -1,8 +1,9 @@ from typing import Optional -from hwprobe.models.component_model import ComponentInfo from pydantic import Field +from hwprobe.models.component_model import ComponentInfo + class CPUInfo(ComponentInfo): #: This is the CPU's name diff --git a/src/hwprobe/models/display_models.py b/src/hwprobe/models/display_models.py index 04721a3..6cf337d 100644 --- a/src/hwprobe/models/display_models.py +++ b/src/hwprobe/models/display_models.py @@ -1,8 +1,9 @@ from typing import Optional -from hwprobe.models.component_model import ComponentInfo from pydantic import BaseModel, Field +from hwprobe.models.component_model import ComponentInfo + class ResolutionInfo(BaseModel): """Resolution information for a Display.""" @@ -19,6 +20,7 @@ class ResolutionInfo(BaseModel): class DisplayModuleInfo(BaseModel): """Information for one Display is stored here""" + name: Optional[str] = None #: Year it was manufactured / designed. diff --git a/src/hwprobe/models/gpu_models.py b/src/hwprobe/models/gpu_models.py index 2d888d1..3ff85b8 100644 --- a/src/hwprobe/models/gpu_models.py +++ b/src/hwprobe/models/gpu_models.py @@ -1,12 +1,14 @@ from typing import Optional +from pydantic import BaseModel, Field + from hwprobe.models.component_model import ComponentInfo from hwprobe.models.size_models import StorageSize -from pydantic import BaseModel, Field class AppleExtendedGPUInfo(BaseModel): """Contains extra information about Apple Silicon GPUs.""" + #: Number of GPU cores. gpu_core_count: Optional[int] = None diff --git a/src/hwprobe/models/info_models.py b/src/hwprobe/models/info_models.py index 2ad1dd3..afcba07 100644 --- a/src/hwprobe/models/info_models.py +++ b/src/hwprobe/models/info_models.py @@ -1,11 +1,12 @@ from typing import Optional +from pydantic import BaseModel + from hwprobe.models.cpu_models import CPUInfo from hwprobe.models.gpu_models import GraphicsInfo from hwprobe.models.memory_models import MemoryInfo from hwprobe.models.network_models import NetworkInfo from hwprobe.models.storage_models import StorageInfo -from pydantic import BaseModel class HardwareInfo(BaseModel): @@ -37,24 +38,18 @@ class HardwareManagerInterface: def fetch_hardware_info(self) -> HardwareInfo: """Fetches all hardware Information.""" - pass def fetch_cpu_info(self) -> CPUInfo: """Fetches CPU Information.""" - pass def fetch_graphics_info(self) -> GraphicsInfo: """Fetches GPU Information.""" - pass def fetch_memory_info(self) -> MemoryInfo: """Fetches RAM Information.""" - pass def fetch_storage_info(self) -> StorageInfo: """Fetches Disk Information.""" - pass def fetch_network_info(self) -> NetworkInfo: """Fetches Network Information.""" - pass diff --git a/src/hwprobe/models/memory_models.py b/src/hwprobe/models/memory_models.py index ffab9d1..0d4e0c7 100644 --- a/src/hwprobe/models/memory_models.py +++ b/src/hwprobe/models/memory_models.py @@ -1,8 +1,9 @@ from typing import Optional +from pydantic import BaseModel, Field + from hwprobe.models.component_model import ComponentInfo from hwprobe.models.size_models import StorageSize -from pydantic import BaseModel, Field class MemoryModuleSlot(BaseModel): diff --git a/src/hwprobe/models/network_models.py b/src/hwprobe/models/network_models.py index ad14ec8..62fa396 100644 --- a/src/hwprobe/models/network_models.py +++ b/src/hwprobe/models/network_models.py @@ -1,8 +1,9 @@ from typing import Optional -from hwprobe.models.component_model import ComponentInfo from pydantic import BaseModel, Field +from hwprobe.models.component_model import ComponentInfo + class NICInfo(BaseModel): name: Optional[str] = None diff --git a/src/hwprobe/models/status_models.py b/src/hwprobe/models/status_models.py index 8b307c0..400d963 100644 --- a/src/hwprobe/models/status_models.py +++ b/src/hwprobe/models/status_models.py @@ -27,6 +27,7 @@ class Status(BaseModel): Describes the status of an individual component. If the status is ``PARTIAL`` or ``FAILED``, there may be messages that describe the error(s). """ + type: StatusType = Field(default_factory=lambda: StatusType.SUCCESS) messages: list[str] = Field(default_factory=list) @@ -38,7 +39,8 @@ def make_partial(self, message: str = None) -> None: :meta private: """ self.type = StatusType.PARTIAL - if message: self.messages.append(message) + if message: + self.messages.append(message) """ diff --git a/src/hwprobe/models/storage_models.py b/src/hwprobe/models/storage_models.py index 8729b3f..02ec81f 100644 --- a/src/hwprobe/models/storage_models.py +++ b/src/hwprobe/models/storage_models.py @@ -1,8 +1,9 @@ from typing import Optional +from pydantic import BaseModel, Field + from hwprobe.models.component_model import ComponentInfo from hwprobe.models.size_models import StorageSize -from pydantic import BaseModel, Field class DiskInfo(BaseModel): diff --git a/src/hwprobe/util/location_paths.py b/src/hwprobe/util/location_paths.py index ef1e471..eeca35e 100644 --- a/src/hwprobe/util/location_paths.py +++ b/src/hwprobe/util/location_paths.py @@ -1,10 +1,10 @@ from ctypes import ( Structure, WinDLL, - c_char, - c_ulong, byref, c_buffer, + c_char, + c_ulong, c_ushort, c_wchar_p, sizeof, @@ -106,11 +106,11 @@ def get_device_instance(pnp_device_id: str) -> c_ulong: def CM_Get_DevNode_PropertyW( - dnDevInst=c_ulong(), - propKey=None, - propType=c_ulong(), - propBuff=None, - propBuffSize=c_ulong(), + dnDevInst=c_ulong(), + propKey=None, + propType=c_ulong(), + propBuff=None, + propBuffSize=c_ulong(), ): if propKey is None: return None @@ -280,7 +280,7 @@ def get_pcie_link_width(pnp_device_id: str) -> Optional[int]: def fetch_device_properties( - pnp_device_id: str, + pnp_device_id: str, ) -> tuple[Optional[list[str]], Optional[str], Optional[str]]: """ Fetch location paths, bus number, and device address in one call. diff --git a/src/hwprobe/util/nvidia.py b/src/hwprobe/util/nvidia.py index 96b9227..6883a27 100644 --- a/src/hwprobe/util/nvidia.py +++ b/src/hwprobe/util/nvidia.py @@ -10,12 +10,7 @@ def fetch_gpu_details_nvidia(device: str) -> tuple[str, int, int, int]: # Fields: Name, PCIe Width, PCIe Gen, Memory Total query_fields = "name,pcie.link.width.current,pcie.link.gen.current,memory.total" - command = [ - "nvidia-smi", - f"--id={device}", - f"--query-gpu={query_fields}", - "--format=csv,noheader,nounits" - ] + command = ["nvidia-smi", f"--id={device}", f"--query-gpu={query_fields}", "--format=csv,noheader,nounits"] # Run the command result = subprocess.run(command, capture_output=True, text=True) @@ -26,7 +21,7 @@ def fetch_gpu_details_nvidia(device: str) -> tuple[str, int, int, int]: # Parse output (Expected: "Name, Width, Gen, Memory") output = result.stdout.strip() - parts = output.split(',') + parts = output.split(",") # Validate we got exactly 4 fields back if len(parts) != 4: diff --git a/tests/core/common/test_edid.py b/tests/core/common/test_edid.py index f971789..a3b99b0 100644 --- a/tests/core/common/test_edid.py +++ b/tests/core/common/test_edid.py @@ -1,6 +1,4 @@ -import struct -import pytest from hwprobe.core.common.edid import parse_edid @@ -31,7 +29,7 @@ def _build_edid( edid = bytearray(128) # Header - edid[0x00:0x08] = b"\x00\xFF\xFF\xFF\xFF\xFF\xFF\x00" + edid[0x00:0x08] = b"\x00\xff\xff\xff\xff\xff\xff\x00" # Manufacturer ID edid[0x08] = manuf[0] @@ -65,32 +63,32 @@ def _build_edid( block[5] = v_active & 0xFF block[6] = v_blank & 0xFF block[7] = ((v_active >> 8) & 0x0F) << 4 | ((v_blank >> 8) & 0x0F) - edid[desc_offset:desc_offset + 18] = block + edid[desc_offset : desc_offset + 18] = block desc_offset += 18 descriptors_used += 1 if name is not None: block = bytearray(18) - block[0:4] = b"\x00\x00\x00\xFC" + block[0:4] = b"\x00\x00\x00\xfc" block[4] = 0x00 name_bytes = name.encode("ascii")[:13] - block[5:5 + len(name_bytes)] = name_bytes + block[5 : 5 + len(name_bytes)] = name_bytes # EDID spec: terminate with 0x0A, pad remainder with 0x20 for i in range(5 + len(name_bytes), 18): block[i] = 0x0A if i == 5 + len(name_bytes) else 0x20 - edid[desc_offset:desc_offset + 18] = block + edid[desc_offset : desc_offset + 18] = block desc_offset += 18 descriptors_used += 1 if serial is not None: block = bytearray(18) - block[0:4] = b"\x00\x00\x00\xFF" + block[0:4] = b"\x00\x00\x00\xff" block[4] = 0x00 serial_bytes = serial.encode("ascii")[:13] - block[5:5 + len(serial_bytes)] = serial_bytes + block[5 : 5 + len(serial_bytes)] = serial_bytes for i in range(5 + len(serial_bytes), 18): block[i] = 0x0A if i == 5 + len(serial_bytes) else 0x20 - edid[desc_offset:desc_offset + 18] = block + edid[desc_offset : desc_offset + 18] = block desc_offset += 18 descriptors_used += 1 @@ -102,7 +100,6 @@ def _build_edid( class TestEdidVersionParsing: - def test_v14_digital_has_bit_depth_and_interface(self): # 0b1_010_0101 = digital, 8-bit depth (010), DisplayPort (5) edid = _build_edid(version=1, revision=4, input_byte=0b10100101) @@ -156,7 +153,6 @@ def test_v12_digital_no_bit_depth_or_interface(self): class TestAnalogDisplay: - def test_analog_interface_set(self): # bit 7 = 0 → analog edid = _build_edid(version=1, revision=3, input_byte=0b00000000) @@ -188,7 +184,8 @@ def test_analog_with_timing_gets_resolution(self): # 1366x768@60Hz: pixel clock = 7622 (in 10kHz units), # h_active=1366, h_blank=434, v_active=768, v_blank=22 edid = _build_edid( - version=1, revision=3, + version=1, + revision=3, input_byte=0b00000000, timing=(7622, 1366, 434, 768, 22), ) @@ -201,7 +198,6 @@ def test_analog_with_timing_gets_resolution(self): class TestCommonFieldsAcrossVersions: - def test_year_parsed(self): edid = _build_edid(year_offset=25) result = parse_edid(edid) diff --git a/tests/core/linux/test_common.py b/tests/core/linux/test_common.py index ad851ce..503092d 100644 --- a/tests/core/linux/test_common.py +++ b/tests/core/linux/test_common.py @@ -1,41 +1,47 @@ import os import pytest -from hwprobe.core.linux.common import pci_path_linux, _format_pci_component + +from hwprobe.core.linux.common import _format_pci_component, pci_path_linux class TestPciPathLinux: def test_single_device(self, monkeypatch): monkeypatch.setattr( - os.path, "realpath", + os.path, + "realpath", lambda _: "/sys/devices/pci0000:00/0000:00:02.0", ) assert pci_path_linux("0000:00:02.0") == "PciRoot(0x0)/Pci(0x2,0x0)" def test_bridge_chain(self, monkeypatch): monkeypatch.setattr( - os.path, "realpath", + os.path, + "realpath", lambda _: "/sys/devices/pci0000:00/0000:00:01.0/0000:01:00.0", ) assert pci_path_linux("0000:01:00.0") == "PciRoot(0x0)/Pci(0x1,0x0)/Pci(0x0,0x0)" def test_multifunction_device(self, monkeypatch): monkeypatch.setattr( - os.path, "realpath", + os.path, + "realpath", lambda _: "/sys/devices/pci0000:00/0000:00:1f.3", ) assert pci_path_linux("0000:00:1f.3") == "PciRoot(0x0)/Pci(0x1f,0x3)" def test_non_zero_domain(self, monkeypatch): monkeypatch.setattr( - os.path, "realpath", + os.path, + "realpath", lambda _: "/sys/devices/pci0001:00/0001:00:00.0", ) assert pci_path_linux("0001:00:00.0") == "PciRoot(0x1)/Pci(0x0,0x0)" def test_fallback_when_sysfs_has_no_pci(self, monkeypatch): monkeypatch.setattr( - os.path, "realpath", + os.path, + "realpath", lambda _: "/sys/devices/platform/non-pci-device", ) assert pci_path_linux("0000:03:00.0") == "PciRoot(0x0)/Pci(0x0,0x0)" diff --git a/tests/core/linux/test_cpu.py b/tests/core/linux/test_cpu.py index 3710788..c21f104 100644 --- a/tests/core/linux/test_cpu.py +++ b/tests/core/linux/test_cpu.py @@ -3,15 +3,15 @@ from hwprobe.core.linux.cpu import ( _arm_cpu_cores, - _x86_cpu_cores, _arm_cpu_model, - _x86_cpu_model, _arm_version, _cpu_threads, + _x86_cpu_cores, + _x86_cpu_model, _x86_flags, fetch_arm_cpu_info, - fetch_x86_cpu_info, fetch_cpu_info, + fetch_x86_cpu_info, ) from hwprobe.models.status_models import StatusType @@ -20,13 +20,7 @@ class TestArmCpuCores: """Tests for _arm_cpu_cores function.""" def test_arm_cpu_cores_success(self, monkeypatch): - output = ( - "# comment\n" - "0,0,0,0\n" - "1,0,0,0\n" - "2,1,0,0\n" - "3,1,0,0\n" - ) + output = "# comment\n0,0,0,0\n1,0,0,0\n2,1,0,0\n3,1,0,0\n" def mock_run(*args, **kwargs): return subprocess.CompletedProcess(args, 0, stdout=output) @@ -37,10 +31,7 @@ def mock_run(*args, **kwargs): assert cores == 2 def test_arm_cpu_cores_single_core(self, monkeypatch): - output = ( - "# comment\n" - "0,0,0,0\n" - ) + output = "# comment\n0,0,0,0\n" def mock_run(*args, **kwargs): return subprocess.CompletedProcess(args, 0, stdout=output) @@ -67,11 +58,7 @@ def test_x86_cpu_cores_success(self): assert _x86_cpu_cores(cpu_lines) == 4 def test_x86_cpu_cores_with_other_info(self): - cpu_lines = ( - "model name\t: Intel CPU\n" - "cpu cores\t: 6\n" - "flags\t\t: sse\n" - ) + cpu_lines = "model name\t: Intel CPU\ncpu cores\t: 6\nflags\t\t: sse\n" assert _x86_cpu_cores(cpu_lines) == 6 def test_x86_cpu_cores_missing(self): @@ -95,10 +82,7 @@ def test_arm_cpu_model_model_field(self): assert _arm_cpu_model(raw) == "Raspberry Pi 4 Model B Rev 1.5" def test_arm_cpu_model_hardware_priority(self): - raw = ( - "Hardware\t: BCM2711\n" - "Model\t: Raspberry Pi 4\n" - ) + raw = "Hardware\t: BCM2711\nModel\t: Raspberry Pi 4\n" # Hardware takes priority over Model assert _arm_cpu_model(raw) == "BCM2711" @@ -147,15 +131,7 @@ def test_cpu_threads_single(self): assert _cpu_threads(raw) == 1 def test_cpu_threads_multiple(self): - raw = ( - "processor\t: 0\n" - "other info\n" - "processor\t: 1\n" - "other info\n" - "processor\t: 2\n" - "other info\n" - "processor\t: 3\n" - ) + raw = "processor\t: 0\nother info\nprocessor\t: 1\nother info\nprocessor\t: 2\nother info\nprocessor\t: 3\n" assert _cpu_threads(raw) == 4 def test_cpu_threads_empty(self): @@ -198,12 +174,7 @@ class TestFetchArmCpuInfo: """Tests for fetch_arm_cpu_info function.""" def test_fetch_arm_cpu_info_success(self, monkeypatch): - raw = ( - "processor\t: 0\n" - "processor\t: 1\n" - "CPU architecture: 8\n" - "Hardware\t: BCM2711\n" - ) + raw = "processor\t: 0\nprocessor\t: 1\nCPU architecture: 8\nHardware\t: BCM2711\n" monkeypatch.setattr( "hwprobe.core.linux.cpu._arm_cpu_cores", @@ -220,11 +191,7 @@ def test_fetch_arm_cpu_info_success(self, monkeypatch): assert cpu.status.messages == [] def test_fetch_arm_cpu_info_model_fallback(self, monkeypatch): - raw = ( - "processor\t: 0\n" - "CPU architecture: 7\n" - "Model\t: Raspberry Pi 4\n" - ) + raw = "processor\t: 0\nCPU architecture: 7\nModel\t: Raspberry Pi 4\n" monkeypatch.setattr("hwprobe.core.linux.cpu._arm_cpu_cores", lambda: 4) @@ -233,10 +200,7 @@ def test_fetch_arm_cpu_info_model_fallback(self, monkeypatch): assert cpu.name == "Raspberry Pi 4" def test_fetch_arm_cpu_info_missing_name(self, monkeypatch): - raw = ( - "processor\t: 0\n" - "CPU architecture: 8\n" - ) + raw = "processor\t: 0\nCPU architecture: 8\n" monkeypatch.setattr("hwprobe.core.linux.cpu._arm_cpu_cores", lambda: 4) @@ -246,10 +210,7 @@ def test_fetch_arm_cpu_info_missing_name(self, monkeypatch): assert "Could not find model name" in cpu.status.messages def test_fetch_arm_cpu_info_missing_arch_version(self, monkeypatch): - raw = ( - "processor\t: 0\n" - "Hardware\t: BCM2711\n" - ) + raw = "processor\t: 0\nHardware\t: BCM2711\n" monkeypatch.setattr("hwprobe.core.linux.cpu._arm_cpu_cores", lambda: 4) @@ -259,10 +220,7 @@ def test_fetch_arm_cpu_info_missing_arch_version(self, monkeypatch): assert "Could not find architecture" in cpu.status.messages def test_fetch_arm_cpu_info_missing_threads(self, monkeypatch): - raw = ( - "Hardware\t: BCM2711\n" - "CPU architecture: 8\n" - ) + raw = "Hardware\t: BCM2711\nCPU architecture: 8\n" monkeypatch.setattr("hwprobe.core.linux.cpu._arm_cpu_cores", lambda: 4) @@ -272,11 +230,7 @@ def test_fetch_arm_cpu_info_missing_threads(self, monkeypatch): assert "Could not find CPU threads" in cpu.status.messages def test_fetch_arm_cpu_info_missing_cores(self, monkeypatch): - raw = ( - "processor\t: 0\n" - "Hardware\t: BCM2711\n" - "CPU architecture: 8\n" - ) + raw = "processor\t: 0\nHardware\t: BCM2711\nCPU architecture: 8\n" monkeypatch.setattr("hwprobe.core.linux.cpu._arm_cpu_cores", lambda: None) @@ -329,47 +283,28 @@ def test_fetch_x86_cpu_info_success(self): assert cpu.status.messages == [] def test_fetch_x86_cpu_info_amd_vendor(self): - raw = ( - "model name\t: AMD Ryzen 5 3600 6-Core Processor\n" - "flags\t\t: sse lm\n" - "cpu cores\t: 6\n" - "\n" - ) + raw = "model name\t: AMD Ryzen 5 3600 6-Core Processor\nflags\t\t: sse lm\ncpu cores\t: 6\n\n" cpu = fetch_x86_cpu_info(raw) assert cpu.vendor == "amd" def test_fetch_x86_cpu_info_unknown_vendor(self): - raw = ( - "model name\t: Generic CPU\n" - "flags\t\t: sse lm\n" - "cpu cores\t: 4\n" - "\n" - ) + raw = "model name\t: Generic CPU\nflags\t\t: sse lm\ncpu cores\t: 4\n\n" cpu = fetch_x86_cpu_info(raw) assert cpu.vendor == "unknown" def test_fetch_x86_cpu_info_32bit(self): - raw = ( - "model name\t: Intel CPU\n" - "flags\t\t: sse sse2\n" - "cpu cores\t: 2\n" - "\n" - ) + raw = "model name\t: Intel CPU\nflags\t\t: sse sse2\ncpu cores\t: 2\n\n" cpu = fetch_x86_cpu_info(raw) assert cpu.bitness == 32 def test_fetch_x86_cpu_info_missing_name(self): - raw = ( - "flags\t\t: sse lm\n" - "cpu cores\t: 4\n" - "\n" - ) + raw = "flags\t\t: sse lm\ncpu cores\t: 4\n\n" cpu = fetch_x86_cpu_info(raw) @@ -377,11 +312,7 @@ def test_fetch_x86_cpu_info_missing_name(self): assert "Could not find CPU name and vendor" in cpu.status.messages def test_fetch_x86_cpu_info_missing_flags(self): - raw = ( - "model name\t: Intel CPU\n" - "cpu cores\t: 4\n" - "\n" - ) + raw = "model name\t: Intel CPU\ncpu cores\t: 4\n\n" cpu = fetch_x86_cpu_info(raw) @@ -391,11 +322,7 @@ def test_fetch_x86_cpu_info_missing_flags(self): assert cpu.bitness == 32 # Default when flags missing def test_fetch_x86_cpu_info_missing_cores(self): - raw = ( - "model name\t: Intel CPU\n" - "flags\t\t: sse lm\n" - "\n" - ) + raw = "model name\t: Intel CPU\nflags\t\t: sse lm\n\n" cpu = fetch_x86_cpu_info(raw) @@ -450,6 +377,7 @@ def test_fetch_cpu_info_x86_success(self, monkeypatch): def mock_open(*args, **kwargs): from io import StringIO + return StringIO(raw) monkeypatch.setattr(builtins, "open", mock_open) @@ -468,6 +396,7 @@ def test_fetch_cpu_info_arm_aarch64(self, monkeypatch): def mock_open(*args, **kwargs): from io import StringIO + return StringIO(raw) monkeypatch.setattr(builtins, "open", mock_open) @@ -488,6 +417,7 @@ def test_fetch_cpu_info_arm_armv7(self, monkeypatch): def mock_open(*args, **kwargs): from io import StringIO + return StringIO(raw) monkeypatch.setattr(builtins, "open", mock_open) @@ -515,6 +445,7 @@ def mock_open(*args, **kwargs): def test_fetch_cpu_info_empty_file(self, monkeypatch): def mock_open(*args, **kwargs): from io import StringIO + return StringIO("") monkeypatch.setattr(builtins, "open", mock_open) @@ -533,6 +464,7 @@ def test_fetch_cpu_info_rpi_success(self, monkeypatch): def mock_open(*args, **kwargs): from io import StringIO + return StringIO(raw) monkeypatch.setattr(builtins, "open", mock_open) @@ -557,6 +489,7 @@ def test_fetch_cpu_info_7200u_success(self, monkeypatch): def mock_open(*args, **kwargs): from io import StringIO + return StringIO(raw) monkeypatch.setattr(builtins, "open", mock_open) diff --git a/tests/core/linux/test_display.py b/tests/core/linux/test_display.py index 8fb5cd0..5592bb3 100644 --- a/tests/core/linux/test_display.py +++ b/tests/core/linux/test_display.py @@ -6,8 +6,8 @@ from hwprobe.core.linux.display import ( _extract_pci_bdf_from_sysfs_path, - _parse_connector_type, _fetch_individual_monitor_info, + _parse_connector_type, fetch_display_info, ) from hwprobe.models.display_models import DisplayModuleInfo @@ -46,7 +46,8 @@ def test_returns_none_when_edid_missing(self, monkeypatch): def test_returns_none_when_edid_empty(self, monkeypatch): self._patch_exists(monkeypatch, {self.EDID_PATH}) monkeypatch.setattr( - builtins, "open", + builtins, + "open", lambda *a, **kw: mock_open(read_data=b"")(), ) assert _fetch_individual_monitor_info(self.DEVICE_PATH) is None @@ -54,7 +55,8 @@ def test_returns_none_when_edid_empty(self, monkeypatch): def test_pci_path_resolved_from_gpu_endpoint(self, monkeypatch): self._patch_exists(monkeypatch, {self.EDID_PATH}) monkeypatch.setattr( - builtins, "open", + builtins, + "open", lambda *a, **kw: mock_open(read_data=b"\x01\x02")(), ) monkeypatch.setattr( @@ -62,7 +64,8 @@ def test_pci_path_resolved_from_gpu_endpoint(self, monkeypatch): lambda _: DisplayModuleInfo(name="Internal Display"), ) monkeypatch.setattr( - os.path, "realpath", + os.path, + "realpath", lambda _: "/sys/devices/pci0000:00/0000:00:02.0/0000:06:00.0/drm/card0", ) @@ -81,7 +84,8 @@ def test_pci_path_resolved_from_gpu_endpoint(self, monkeypatch): def test_no_pci_path_for_non_pci_parent(self, monkeypatch): self._patch_exists(monkeypatch, {self.EDID_PATH}) monkeypatch.setattr( - builtins, "open", + builtins, + "open", lambda *a, **kw: mock_open(read_data=b"\x01\x02")(), ) monkeypatch.setattr( @@ -89,7 +93,8 @@ def test_no_pci_path_for_non_pci_parent(self, monkeypatch): lambda _: DisplayModuleInfo(name="Panel"), ) monkeypatch.setattr( - os.path, "realpath", + os.path, + "realpath", lambda _: "/sys/devices/platform/simple-framebuffer/drm/card0", ) @@ -123,7 +128,8 @@ def fake_open(path, *args, **kwargs): lambda _: DisplayModuleInfo(name="Display"), ) monkeypatch.setattr( - os.path, "realpath", + os.path, + "realpath", lambda _: "/sys/devices/pci0000:00/0000:00:02.0/drm/card0", ) monkeypatch.setattr( @@ -141,7 +147,8 @@ class TestFetchDisplayInfo: def test_collects_monitors_from_drm(self, monkeypatch): monkeypatch.setattr(os.path, "isdir", lambda p: p == "/sys/class/drm") monkeypatch.setattr( - os, "listdir", + os, + "listdir", lambda path: { "/sys/class/drm": ["card0", "renderD128", "version"], "/sys/class/drm/card0": ["card0-eDP-1", "card0-HDMI-A-1", "device"], @@ -161,7 +168,8 @@ def test_collects_monitors_from_drm(self, monkeypatch): def test_skips_monitors_returning_none(self, monkeypatch): monkeypatch.setattr(os.path, "isdir", lambda p: p == "/sys/class/drm") monkeypatch.setattr( - os, "listdir", + os, + "listdir", lambda path: { "/sys/class/drm": ["card0"], "/sys/class/drm/card0": ["card0-eDP-1"], @@ -188,7 +196,8 @@ def test_failed_when_drm_root_missing(self, monkeypatch): def test_partial_when_monitor_raises(self, monkeypatch): monkeypatch.setattr(os.path, "isdir", lambda p: p == "/sys/class/drm") monkeypatch.setattr( - os, "listdir", + os, + "listdir", lambda path: { "/sys/class/drm": ["card0"], "/sys/class/drm/card0": ["card0-eDP-1", "card0-HDMI-A-1"], @@ -204,7 +213,8 @@ def _mock_fetch(path): return DisplayModuleInfo(name="Monitor B") monkeypatch.setattr( - "hwprobe.core.linux.display._fetch_individual_monitor_info", _mock_fetch, + "hwprobe.core.linux.display._fetch_individual_monitor_info", + _mock_fetch, ) info = fetch_display_info() @@ -216,20 +226,22 @@ def _mock_fetch(path): class TestParseConnectorType: - - @pytest.mark.parametrize("dirname,expected", [ - ("card0-eDP-1", "DisplayPort"), - ("card0-DP-1", "DisplayPort"), - ("card0-DP-2", "DisplayPort"), - ("card1-HDMI-A-1", "HDMI"), - ("card0-HDMI-B-1", "HDMI (B)"), - ("card0-DVI-D-1", "DVI"), - ("card0-DVI-I-1", "DVI"), - ("card0-DVI-A-1", "DVI"), - ("card0-VGA-1", "Analog"), - ("card0-LVDS-1", "LVDS"), - ("card0-DSI-1", "DSI"), - ]) + @pytest.mark.parametrize( + "dirname,expected", + [ + ("card0-eDP-1", "DisplayPort"), + ("card0-DP-1", "DisplayPort"), + ("card0-DP-2", "DisplayPort"), + ("card1-HDMI-A-1", "HDMI"), + ("card0-HDMI-B-1", "HDMI (B)"), + ("card0-DVI-D-1", "DVI"), + ("card0-DVI-I-1", "DVI"), + ("card0-DVI-A-1", "DVI"), + ("card0-VGA-1", "Analog"), + ("card0-LVDS-1", "LVDS"), + ("card0-DSI-1", "DSI"), + ], + ) def test_known_connectors(self, dirname, expected): path = f"/sys/class/drm/card0/{dirname}" assert _parse_connector_type(path) == expected @@ -248,7 +260,8 @@ def test_connector_overrides_edid_interface(self, monkeypatch): monkeypatch.setattr(os.path, "exists", lambda p: p == edid_path) monkeypatch.setattr( - builtins, "open", + builtins, + "open", lambda *a, **kw: mock_open(read_data=b"\x01\x02")(), ) monkeypatch.setattr( @@ -256,7 +269,8 @@ def test_connector_overrides_edid_interface(self, monkeypatch): lambda _: DisplayModuleInfo(name="Test", interface="DisplayPort"), ) monkeypatch.setattr( - os.path, "realpath", + os.path, + "realpath", lambda _: "/sys/devices/platform/drm/card0", ) diff --git a/tests/core/linux/test_graphics.py b/tests/core/linux/test_graphics.py index b9d74f4..81330e4 100644 --- a/tests/core/linux/test_graphics.py +++ b/tests/core/linux/test_graphics.py @@ -4,12 +4,12 @@ from unittest.mock import mock_open from hwprobe.core.linux.graphics import ( - _vram_amd, - _pcie_gen, _check_gpu_class, + _pcie_gen, _populate_amd_info, - _populate_nvidia_info, _populate_lspci_info, + _populate_nvidia_info, + _vram_amd, fetch_graphics_info, ) from hwprobe.models.gpu_models import GPUInfo @@ -75,7 +75,7 @@ def test_vram_amd_read_error(self, monkeypatch): monkeypatch.setattr("glob.glob", lambda x: [vram_path]) def mock_open_func(file, *args, **kwargs): - raise IOError("Read error") + raise OSError("Read error") monkeypatch.setattr(builtins, "open", mock_open_func) @@ -212,7 +212,7 @@ def test_pcie_gen_read_exception(self, monkeypatch): monkeypatch.setattr(os.path, "exists", lambda x: x == path) def mock_open_func(file, *args, **kwargs): - raise IOError("Read error") + raise OSError("Read error") monkeypatch.setattr(builtins, "open", mock_open_func) @@ -314,9 +314,7 @@ def test_populate_nvidia_info_success(self, monkeypatch): def mock_run(command, *args, **kwargs): if command[0] == "nvidia-smi": - return subprocess.CompletedProcess( - command, 0, stdout="GeForce RTX 3080, 16, 4, 10240\n" - ) + return subprocess.CompletedProcess(command, 0, stdout="GeForce RTX 3080, 16, 4, 10240\n") return subprocess.CompletedProcess(command, 1) monkeypatch.setattr(subprocess, "run", mock_run) @@ -378,10 +376,7 @@ def test_populate_lspci_info_minimal(self, monkeypatch): def mock_run(command, *args, **kwargs): if command[0] == "lspci": - output = ( - "Vendor:\tIntel Corporation\n" - "Device:\tUHD Graphics 620\n" - ) + output = "Vendor:\tIntel Corporation\nDevice:\tUHD Graphics 620\n" return subprocess.CompletedProcess(command, 0, stdout=output) return subprocess.CompletedProcess(command, 1) @@ -432,7 +427,7 @@ def test_fetch_graphics_info_success_intel(self, monkeypatch): "device": "0x5917", "current_link_width": "0", "current_link_speed": "8.0 GT/s", - "firmware_node/path": "\\_SB.PCI0.GFX0" + "firmware_node/path": "\\_SB.PCI0.GFX0", } def custom_open(path, *args, **kwargs): @@ -444,16 +439,11 @@ def custom_open(path, *args, **kwargs): raise FileNotFoundError(path) monkeypatch.setattr(builtins, "open", custom_open) - monkeypatch.setattr("hwprobe.core.linux.graphics.pci_path_linux", lambda x: f"PciRoot(0x0)/Pci(0x2,0x0)") + monkeypatch.setattr("hwprobe.core.linux.graphics.pci_path_linux", lambda x: "PciRoot(0x0)/Pci(0x2,0x0)") def mock_run(command, *args, **kwargs): if command[0] == "lspci": - output = ( - "Vendor:\tIntel Corporation\n" - "Device:\tUHD Graphics 620\n" - "SVendor:\tLenovo\n" - "SDevice:\tThinkPad\n" - ) + output = "Vendor:\tIntel Corporation\nDevice:\tUHD Graphics 620\nSVendor:\tLenovo\nSDevice:\tThinkPad\n" return subprocess.CompletedProcess(command, 0, stdout=output) return subprocess.CompletedProcess(command, 1) @@ -482,7 +472,7 @@ def test_fetch_graphics_info_nvidia(self, monkeypatch): "device": "0x1c03", "current_link_width": "16", "current_link_speed": "8.0 GT/s", - "firmware_node/path": "\\_SB.PCI0.PEG0.PEGP" + "firmware_node/path": "\\_SB.PCI0.PEG0.PEGP", } def custom_open(path, *args, **kwargs): @@ -524,7 +514,7 @@ def test_fetch_graphics_info_amd(self, monkeypatch): "device": "0x731f", "current_link_width": "16", "current_link_speed": "16.0 GT/s", - "firmware_node/path": "\\_SB.PCI0.PEG0.PEGP" + "firmware_node/path": "\\_SB.PCI0.PEG0.PEGP", } def custom_open(path, *args, **kwargs): @@ -540,8 +530,7 @@ def custom_open(path, *args, **kwargs): monkeypatch.setattr(builtins, "open", custom_open) monkeypatch.setattr("hwprobe.core.linux.graphics.pci_path_linux", lambda x: "PciRoot(0x0)/Pci(0x3,0x0)") monkeypatch.setattr( - "glob.glob", - lambda x: ["/sys/bus/pci/devices/0000:03:00.0/drm/card0/device/mem_info_vram_total"] + "glob.glob", lambda x: ["/sys/bus/pci/devices/0000:03:00.0/drm/card0/device/mem_info_vram_total"] ) def mock_run(command, *args, **kwargs): @@ -591,10 +580,10 @@ def custom_open(path, *args, **kwargs): if filename == "class": return mock_open(read_data="0x030000")() if filename == "vendor": - raise IOError("Permission denied") + raise OSError("Permission denied") if filename == "device": return mock_open(read_data="0x1234")() - raise IOError("File not found") + raise OSError("File not found") monkeypatch.setattr(builtins, "open", custom_open) @@ -651,7 +640,7 @@ def test_fetch_graphics_info_pci_path_failure(self, monkeypatch): "device": "0x5917", "current_link_width": "0", "current_link_speed": "8.0 GT/s", - "firmware_node/path": "\\_SB.PCI0.GFX0" + "firmware_node/path": "\\_SB.PCI0.GFX0", } def custom_open(path, *args, **kwargs): @@ -688,7 +677,7 @@ def test_fetch_graphics_info_nvidia_failure(self, monkeypatch): "device": "0x1c03", "current_link_width": "16", "current_link_speed": "8.0 GT/s", - "firmware_node/path": "\\_SB.PCI0.PEG0.PEGP" + "firmware_node/path": "\\_SB.PCI0.PEG0.PEGP", } def custom_open(path, *args, **kwargs): @@ -728,7 +717,7 @@ def test_fetch_graphics_info_lspci_failure(self, monkeypatch): "device": "0x5917", "current_link_width": "0", "current_link_speed": "8.0 GT/s", - "firmware_node/path": "\\_SB.PCI0.GFX0" + "firmware_node/path": "\\_SB.PCI0.GFX0", } def custom_open(path, *args, **kwargs): @@ -766,7 +755,7 @@ def test_fetch_graphics_info_pcie_gen_failure(self, monkeypatch): "vendor": "0x8086", "device": "0x5917", "current_link_width": "0", - "firmware_node/path": "\\_SB.PCI0.GFX0" + "firmware_node/path": "\\_SB.PCI0.GFX0", } def custom_open(path, *args, **kwargs): @@ -795,7 +784,7 @@ def test_fetch_graphics_info_class_read_failure(self, monkeypatch): def custom_open(path, *args, **kwargs): if "class" in path: - raise IOError("Permission denied") + raise OSError("Permission denied") raise FileNotFoundError(path) monkeypatch.setattr(builtins, "open", custom_open) diff --git a/tests/core/linux/test_memory.py b/tests/core/linux/test_memory.py index e8d5d92..d8438e6 100644 --- a/tests/core/linux/test_memory.py +++ b/tests/core/linux/test_memory.py @@ -3,16 +3,16 @@ from unittest.mock import MagicMock from hwprobe.core.linux.memory import ( - fetch_memory_info, - _part_no, - _dimm_type, - _dimm_slot, _dimm_capacity, - _ecc_support, + _dimm_slot, _dimm_speed, + _dimm_type, + _ecc_support, + _part_no, + fetch_memory_info, ) from hwprobe.models.memory_models import MemoryModuleSlot -from hwprobe.models.size_models import Megabyte, Kilobyte +from hwprobe.models.size_models import Kilobyte, Megabyte from hwprobe.models.status_models import StatusType @@ -111,7 +111,7 @@ class TestDimmCapacity: def test_dimm_capacity_megabytes(self): value = bytearray(0x20) size_mb = 8192 # 8 GB - value[0x0C:0x0E] = size_mb.to_bytes(2, 'little') + value[0x0C:0x0E] = size_mb.to_bytes(2, "little") result = _dimm_capacity(bytes(value)) assert result is not None @@ -122,7 +122,7 @@ def test_dimm_capacity_kilobytes(self): value = bytearray(0x20) # Bit 15 set means kilobytes size_kb = 2048 | 0x8000 - value[0x0C:0x0E] = size_kb.to_bytes(2, 'little') + value[0x0C:0x0E] = size_kb.to_bytes(2, "little") result = _dimm_capacity(bytes(value)) assert result is not None @@ -131,8 +131,8 @@ def test_dimm_capacity_kilobytes(self): def test_dimm_capacity_extended_size(self): value = bytearray(0x20) - value[0x0C:0x0E] = (0x7FFF).to_bytes(2, 'little') # Use extended size - value[0x1C:0x20] = (32768).to_bytes(4, 'little') # 32 GB + value[0x0C:0x0E] = (0x7FFF).to_bytes(2, "little") # Use extended size + value[0x1C:0x20] = (32768).to_bytes(4, "little") # 32 GB result = _dimm_capacity(bytes(value)) assert result is not None @@ -141,7 +141,7 @@ def test_dimm_capacity_extended_size(self): def test_dimm_capacity_unknown(self): value = bytearray(0x20) - value[0x0C:0x0E] = (0xFFFF).to_bytes(2, 'little') # Unknown size + value[0x0C:0x0E] = (0xFFFF).to_bytes(2, "little") # Unknown size result = _dimm_capacity(bytes(value)) assert result is None @@ -152,16 +152,16 @@ class TestEccSupport: def test_ecc_support_true(self): value = bytearray(0x0C) - value[0x08:0x0A] = (72).to_bytes(2, 'little') # Total width - value[0x0A:0x0C] = (64).to_bytes(2, 'little') # Data width + value[0x08:0x0A] = (72).to_bytes(2, "little") # Total width + value[0x0A:0x0C] = (64).to_bytes(2, "little") # Data width result = _ecc_support(bytes(value)) assert result is True def test_ecc_support_false(self): value = bytearray(0x0C) - value[0x08:0x0A] = (64).to_bytes(2, 'little') # Total width - value[0x0A:0x0C] = (64).to_bytes(2, 'little') # Data width + value[0x08:0x0A] = (64).to_bytes(2, "little") # Total width + value[0x0A:0x0C] = (64).to_bytes(2, "little") # Data width result = _ecc_support(bytes(value)) assert result is False @@ -172,29 +172,28 @@ class TestDimmSpeed: def test_dimm_speed_normal(self): value = bytearray(0x58) - value[0x15:0x17] = (3200).to_bytes(2, 'little') + value[0x15:0x17] = (3200).to_bytes(2, "little") result = _dimm_speed(bytes(value)) assert result == 3200 def test_dimm_speed_extended(self): value = bytearray(0x58) - value[0x15:0x17] = (0xFFFF).to_bytes(2, 'little') # Use extended speed - value[0x54:0x58] = (4800).to_bytes(4, 'little') + value[0x15:0x17] = (0xFFFF).to_bytes(2, "little") # Use extended speed + value[0x54:0x58] = (4800).to_bytes(4, "little") result = _dimm_speed(bytes(value)) assert result == 4800 def test_dimm_speed_unknown(self): value = bytearray(0x58) - value[0x15:0x17] = (0).to_bytes(2, 'little') # Unknown speed + value[0x15:0x17] = (0).to_bytes(2, "little") # Unknown speed result = _dimm_speed(bytes(value)) assert result is None class TestLinuxMemory: - def test_fetch_memory_info_no_dmi_dir(self, monkeypatch): monkeypatch.setattr(os.path, "isdir", lambda x: False) @@ -225,18 +224,20 @@ def mock_open(*args, **kwargs): assert memory_info.status.type == StatusType.FAILED assert memory_info.status.messages is not None - def _create_dmi_blob(self, - size_mb=8192, - total_width=72, - data_width=64, - speed=3200, - mem_type=0x1A, # DDR4 - part_no="1234-5678", - dev_loc="DIMM 0", - bank_loc="BANK 0", - manufacturer="Acme Corp", - extended_size=None, - extended_speed=None): + def _create_dmi_blob( + self, + size_mb=8192, + total_width=72, + data_width=64, + speed=3200, + mem_type=0x1A, # DDR4 + part_no="1234-5678", + dev_loc="DIMM 0", + bank_loc="BANK 0", + manufacturer="Acme Corp", + extended_size=None, + extended_speed=None, + ): # Header length length = 0x5C @@ -245,13 +246,13 @@ def _create_dmi_blob(self, data[0x01] = length # Strings - include "DIMM" in the data for _part_no check - strings_bytes = b'' + strings_bytes = b"" string_indices = {} current_index = 1 for s in [dev_loc, bank_loc, manufacturer, part_no]: if s: - strings_bytes += s.encode('ascii') + b'\0' + strings_bytes += s.encode("ascii") + b"\0" string_indices[s] = current_index current_index += 1 @@ -265,26 +266,26 @@ def _create_dmi_blob(self, data[0x12] = mem_type # Set Widths - data[0x08:0x0A] = total_width.to_bytes(2, 'little') - data[0x0A:0x0C] = data_width.to_bytes(2, 'little') + data[0x08:0x0A] = total_width.to_bytes(2, "little") + data[0x0A:0x0C] = data_width.to_bytes(2, "little") # Set Size if extended_size is not None: - data[0x0C:0x0E] = (0x7FFF).to_bytes(2, 'little') - data[0x1C:0x20] = extended_size.to_bytes(4, 'little') + data[0x0C:0x0E] = (0x7FFF).to_bytes(2, "little") + data[0x1C:0x20] = extended_size.to_bytes(4, "little") else: # Normal size - Bit 15 = 0 for MB. - data[0x0C:0x0E] = size_mb.to_bytes(2, 'little') + data[0x0C:0x0E] = size_mb.to_bytes(2, "little") # Set Speed if extended_speed is not None: - data[0x15:0x17] = (0xFFFF).to_bytes(2, 'little') - data[0x54:0x58] = extended_speed.to_bytes(4, 'little') + data[0x15:0x17] = (0xFFFF).to_bytes(2, "little") + data[0x54:0x58] = extended_speed.to_bytes(4, "little") else: - data[0x15:0x17] = speed.to_bytes(2, 'little') + data[0x15:0x17] = speed.to_bytes(2, "little") # Double null terminator at end of strings - strings_bytes += b'\0' + strings_bytes += b"\0" return bytes(data) + strings_bytes @@ -304,6 +305,7 @@ def mock_scandir(path): def mock_open(*args, **kwargs): from io import BytesIO + return BytesIO(blob) monkeypatch.setattr(builtins, "open", mock_open) @@ -337,6 +339,7 @@ def test_fetch_memory_info_non_ecc(self, monkeypatch): def mock_open(*args, **kwargs): from io import BytesIO + return BytesIO(blob) monkeypatch.setattr(builtins, "open", mock_open) @@ -355,11 +358,12 @@ def test_fetch_memory_info_unknown_size(self, monkeypatch): blob = self._create_dmi_blob() # Manually overwrite size to 0xFFFF data = bytearray(blob) - data[0x0C:0x0E] = (0xFFFF).to_bytes(2, 'little') + data[0x0C:0x0E] = (0xFFFF).to_bytes(2, "little") blob = bytes(data) def mock_open(*args, **kwargs): from io import BytesIO + return BytesIO(blob) monkeypatch.setattr(builtins, "open", mock_open) @@ -382,6 +386,7 @@ def test_fetch_memory_info_extended_speed(self, monkeypatch): def mock_open(*args, **kwargs): from io import BytesIO + return BytesIO(blob) monkeypatch.setattr(builtins, "open", mock_open) @@ -399,7 +404,8 @@ def test_fetch_memory_info_parsing_error(self, monkeypatch): # Return garbage that contains "DIMM" to trigger the parsing logic def mock_open(*args, **kwargs): from io import BytesIO - return BytesIO(b'DIMM') + + return BytesIO(b"DIMM") monkeypatch.setattr(builtins, "open", mock_open) @@ -421,10 +427,11 @@ def test_fetch_memory_info_kilobyte_capacity(self, monkeypatch): blob = self._create_dmi_blob() data = bytearray(blob) - data[0x0C:0x0E] = size_kb_val.to_bytes(2, 'little') + data[0x0C:0x0E] = size_kb_val.to_bytes(2, "little") def mock_open(*args, **kwargs): from io import BytesIO + return BytesIO(bytes(data)) monkeypatch.setattr(builtins, "open", mock_open) @@ -449,6 +456,7 @@ def test_fetch_memory_info_type_error(self, monkeypatch): def mock_open(*args, **kwargs): from io import BytesIO + return BytesIO(bytes(data)) monkeypatch.setattr(builtins, "open", mock_open) @@ -473,6 +481,7 @@ def test_fetch_memory_info_location_error(self, monkeypatch): def mock_open(*args, **kwargs): from io import BytesIO + return BytesIO(bytes(data)) monkeypatch.setattr(builtins, "open", mock_open) @@ -496,6 +505,7 @@ def test_fetch_memory_info_manufacturer_error(self, monkeypatch): def mock_open(*args, **kwargs): from io import BytesIO + return BytesIO(bytes(data)) monkeypatch.setattr(builtins, "open", mock_open) @@ -516,6 +526,7 @@ def test_fetch_memory_info_capacity_error(self, monkeypatch): def mock_open(*args, **kwargs): from io import BytesIO + return BytesIO(blob) monkeypatch.setattr(builtins, "open", mock_open) diff --git a/tests/core/linux/test_storage.py b/tests/core/linux/test_storage.py index 6325a4e..a0a5f81 100644 --- a/tests/core/linux/test_storage.py +++ b/tests/core/linux/test_storage.py @@ -7,7 +7,6 @@ class TestLinuxStorage: - def test_fetch_storage_info_no_sys_block(self, monkeypatch): monkeypatch.setattr(os.path, "isdir", lambda x: False) @@ -26,9 +25,7 @@ def mock_open(path, mode="r"): content = "" if "nvme0n1/device/model" in path: content = "Samsung SSD 970 EVO Plus 1TB" - elif "nvme0n1/queue/rotational" in path: - content = "0" - elif "nvme0n1/removable" in path: + elif "nvme0n1/queue/rotational" in path or "nvme0n1/removable" in path: content = "0" elif "nvme0n1/device/device/device" in path: content = "0xa808" @@ -258,18 +255,34 @@ def mock_open(path, mode="r"): def test_fetch_storage_info_filters_partitions_and_boot_devices(self, monkeypatch): monkeypatch.setattr(os.path, "isdir", lambda x: True) - monkeypatch.setattr(os, "listdir", lambda x: [ - "sda", "sda1", "sda2", # sda is disk, sda1/sda2 are partitions - "mmcblk0", "mmcblk0p1", "mmcblk0boot0", "mmcblk0boot1", "mmcblk0rpmb", - # mmcblk0 is disk, others should be filtered - "nvme0n1", "nvme0n1p1", "nvme0n1p2" # nvme0n1 is disk, partitions should be filtered - ]) + monkeypatch.setattr( + os, + "listdir", + lambda x: [ + "sda", + "sda1", + "sda2", # sda is disk, sda1/sda2 are partitions + "mmcblk0", + "mmcblk0p1", + "mmcblk0boot0", + "mmcblk0boot1", + "mmcblk0rpmb", + # mmcblk0 is disk, others should be filtered + "nvme0n1", + "nvme0n1p1", + "nvme0n1p2", # nvme0n1 is disk, partitions should be filtered + ], + ) def mock_exists(path): # Only partition files exist for actual partitions - return ("sda1/partition" in path or "sda2/partition" in path or - "mmcblk0p1/partition" in path or - "nvme0n1p1/partition" in path or "nvme0n1p2/partition" in path) + return ( + "sda1/partition" in path + or "sda2/partition" in path + or "mmcblk0p1/partition" in path + or "nvme0n1p1/partition" in path + or "nvme0n1p2/partition" in path + ) monkeypatch.setattr(os.path, "exists", mock_exists) @@ -280,9 +293,7 @@ def mock_open(path, mode="r"): # Mock for sda if "sda/device/model" in path: content = "Test SSD" - elif "sda/queue/rotational" in path: - content = "0" - elif "sda/removable" in path: + elif "sda/queue/rotational" in path or "sda/removable" in path: content = "0" elif "sda/device/vendor" in path: content = "TestVendor" @@ -304,9 +315,7 @@ def mock_open(path, mode="r"): # Mock for nvme0n1 elif "nvme0n1/device/model" in path: content = "Test NVMe" - elif "nvme0n1/queue/rotational" in path: - content = "0" - elif "nvme0n1/removable" in path: + elif "nvme0n1/queue/rotational" in path or "nvme0n1/removable" in path: content = "0" elif "nvme0n1/device/device/device" in path: content = "0x1234" diff --git a/tests/core/mac/test_cpu.py b/tests/core/mac/test_cpu.py index fd5c940..25f5994 100644 --- a/tests/core/mac/test_cpu.py +++ b/tests/core/mac/test_cpu.py @@ -47,8 +47,10 @@ # ── helpers ────────────────────────────────────────────────────────────────── -def _mock_check_output(sysctl_cpu, arch="arm64", bitness=SYSCTL_BITNESS_64, - sme=SYSCTL_SME_ABSENT, sme2=SYSCTL_SME2_ABSENT): + +def _mock_check_output( + sysctl_cpu, arch="arm64", bitness=SYSCTL_BITNESS_64, sme=SYSCTL_SME_ABSENT, sme2=SYSCTL_SME2_ABSENT +): """Build a side_effect for subprocess.check_output that returns the right value depending on the command argument.""" @@ -70,8 +72,8 @@ def side_effect(cmd): # ── Apple Silicon happy path ───────────────────────────────────────────────── -class TestAppleSiliconCPU: +class TestAppleSiliconCPU: @patch("hwprobe.core.mac.cpu.subprocess.check_output") def test_apple_m3_basic_info(self, mock_co): mock_co.side_effect = _mock_check_output(SYSCTL_APPLE_M3) @@ -86,25 +88,19 @@ def test_apple_m3_basic_info(self, mock_co): @patch("hwprobe.core.mac.cpu.subprocess.check_output") def test_apple_silicon_arm_v9_detected(self, mock_co): - mock_co.side_effect = _mock_check_output( - SYSCTL_APPLE_M3, sme=SYSCTL_SME_PRESENT - ) + mock_co.side_effect = _mock_check_output(SYSCTL_APPLE_M3, sme=SYSCTL_SME_PRESENT) info = fetch_cpu_info() assert info.arch_version == "9" @patch("hwprobe.core.mac.cpu.subprocess.check_output") def test_apple_silicon_arm_v8_detected(self, mock_co): - mock_co.side_effect = _mock_check_output( - SYSCTL_APPLE_M3, sme=SYSCTL_SME_ABSENT, sme2=SYSCTL_SME2_ABSENT - ) + mock_co.side_effect = _mock_check_output(SYSCTL_APPLE_M3, sme=SYSCTL_SME_ABSENT, sme2=SYSCTL_SME2_ABSENT) info = fetch_cpu_info() assert info.arch_version == "8" @patch("hwprobe.core.mac.cpu.subprocess.check_output") def test_apple_silicon_sme2_alone_triggers_v9(self, mock_co): - mock_co.side_effect = _mock_check_output( - SYSCTL_APPLE_M3, sme=SYSCTL_SME_ABSENT, sme2=SYSCTL_SME2_PRESENT - ) + mock_co.side_effect = _mock_check_output(SYSCTL_APPLE_M3, sme=SYSCTL_SME_ABSENT, sme2=SYSCTL_SME2_PRESENT) info = fetch_cpu_info() assert info.arch_version == "9" @@ -118,8 +114,8 @@ def test_apple_silicon_no_sse_flags(self, mock_co): # ── Intel happy path ───────────────────────────────────────────────────────── -class TestIntelCPU: +class TestIntelCPU: @patch("hwprobe.core.mac.cpu.subprocess.check_output") def test_intel_basic_info(self, mock_co): mock_co.side_effect = _mock_check_output(SYSCTL_INTEL, arch="x86_64") @@ -158,17 +154,15 @@ def test_intel_no_arm_version(self, mock_co): @patch("hwprobe.core.mac.cpu.subprocess.check_output") def test_intel_32bit(self, mock_co): - mock_co.side_effect = _mock_check_output( - SYSCTL_INTEL, arch="i386", bitness=SYSCTL_BITNESS_32 - ) + mock_co.side_effect = _mock_check_output(SYSCTL_INTEL, arch="i386", bitness=SYSCTL_BITNESS_32) info = fetch_cpu_info() assert info.bitness == 32 # ── AMD ────────────────────────────────────────────────────────────────────── -class TestAMDCPU: +class TestAMDCPU: @patch("hwprobe.core.mac.cpu.subprocess.check_output") def test_amd_vendor_detected(self, mock_co): mock_co.side_effect = _mock_check_output(SYSCTL_AMD, arch="x86_64") @@ -180,8 +174,8 @@ def test_amd_vendor_detected(self, mock_co): # ── Error handling ─────────────────────────────────────────────────────────── -class TestErrorHandling: +class TestErrorHandling: @patch("hwprobe.core.mac.cpu.subprocess.check_output") def test_sysctl_failure_returns_failed(self, mock_co): mock_co.side_effect = FileNotFoundError("sysctl not found") @@ -236,6 +230,7 @@ def test_empty_uname_output_is_partial(self, mock_co): # ── BUG: sysctl output with trailing empty line crashes split ──────────────── + class TestSysctlParsingEdgeCases: """Malformed sysctl lines (without ': ') should be skipped, not crash.""" @@ -258,16 +253,14 @@ def test_sysctl_line_without_separator_is_skipped(self, mock_co): # ── BUG: KeyError when both vendor and brand_string are absent ─────────────── + class TestMissingBrandString: """When both vendor and brand_string are absent, vendor should remain None without crashing.""" @patch("hwprobe.core.mac.cpu.subprocess.check_output") def test_missing_vendor_and_brand_string_no_crash(self, mock_co): - minimal_sysctl = ( - "machdep.cpu.core_count: 4\n" - "machdep.cpu.thread_count: 4\n" - ) + minimal_sysctl = "machdep.cpu.core_count: 4\nmachdep.cpu.thread_count: 4\n" mock_co.side_effect = _mock_check_output(minimal_sysctl, arch="x86_64") info = fetch_cpu_info() assert info.vendor is None @@ -276,6 +269,7 @@ def test_missing_vendor_and_brand_string_no_crash(self, mock_co): # ── BUG: Inconsistent arch casing in ARM version detection ─────────────────── + class TestArchCasingConsistency: """ARM version detection should work regardless of uname casing.""" @@ -290,14 +284,11 @@ def test_uppercase_arm_still_detects_version(self, mock_co): # ── Missing cores/threads ─────────────────────────────────────────────────── -class TestMissingCoresThreads: +class TestMissingCoresThreads: @patch("hwprobe.core.mac.cpu.subprocess.check_output") def test_missing_core_count_is_partial(self, mock_co): - sysctl = ( - "machdep.cpu.brand_string: Apple M3\n" - "machdep.cpu.thread_count: 8\n" - ) + sysctl = "machdep.cpu.brand_string: Apple M3\nmachdep.cpu.thread_count: 8\n" mock_co.side_effect = _mock_check_output(sysctl) info = fetch_cpu_info() assert info.cores is None @@ -306,10 +297,7 @@ def test_missing_core_count_is_partial(self, mock_co): @patch("hwprobe.core.mac.cpu.subprocess.check_output") def test_missing_thread_count_is_partial(self, mock_co): - sysctl = ( - "machdep.cpu.brand_string: Apple M3\n" - "machdep.cpu.core_count: 8\n" - ) + sysctl = "machdep.cpu.brand_string: Apple M3\nmachdep.cpu.core_count: 8\n" mock_co.side_effect = _mock_check_output(sysctl) info = fetch_cpu_info() assert info.cores == 8 @@ -319,11 +307,12 @@ def test_missing_thread_count_is_partial(self, mock_co): # ── Return type ────────────────────────────────────────────────────────────── -class TestReturnType: +class TestReturnType: @patch("hwprobe.core.mac.cpu.subprocess.check_output") def test_return_type_is_cpu_info(self, mock_co): from hwprobe.models.cpu_models import CPUInfo + mock_co.side_effect = _mock_check_output(SYSCTL_APPLE_M3) info = fetch_cpu_info() assert isinstance(info, CPUInfo) diff --git a/tests/core/mac/test_display.py b/tests/core/mac/test_display.py index 3f0142f..8e0f100 100644 --- a/tests/core/mac/test_display.py +++ b/tests/core/mac/test_display.py @@ -1,37 +1,43 @@ import json -from unittest.mock import patch, MagicMock +from unittest.mock import MagicMock, patch from hwprobe.core.mac.display import ( - _get_monitor_resolution_from_system_profiler, - _get_refresh_rate_from_system_profiler, _enrich_data_from_edid, _fetch_monitor_info_system_profiler, + _get_monitor_resolution_from_system_profiler, + _get_refresh_rate_from_system_profiler, fetch_display_info, ) from hwprobe.models.display_models import DisplayModuleInfo from hwprobe.models.status_models import StatusType - # ── sample system_profiler JSON structures ─────────────────────────────────── + def _make_sp_output(monitors_per_controller=None): """Build a fake system_profiler SPDisplaysDataType JSON structure.""" if monitors_per_controller is None: - monitors_per_controller = [[{ - "_name": "Built-in Retina Display", - "spdisplays_pixelresolution": "3024 x 1964 @ 120.00Hz", - "_spdisplays_display-serial-number": "SN12345", - "_spdisplays_display-year": "2023", - "sppci_model": "Apple M3 Pro", - }]] + monitors_per_controller = [ + [ + { + "_name": "Built-in Retina Display", + "spdisplays_pixelresolution": "3024 x 1964 @ 120.00Hz", + "_spdisplays_display-serial-number": "SN12345", + "_spdisplays_display-year": "2023", + "sppci_model": "Apple M3 Pro", + } + ] + ] controllers = [] for i, monitors in enumerate(monitors_per_controller): - controllers.append({ - "_name": f"Controller {i}", - "sppci_model": f"GPU {i}", - "spdisplays_ndrvs": monitors, - }) + controllers.append( + { + "_name": f"Controller {i}", + "sppci_model": f"GPU {i}", + "spdisplays_ndrvs": monitors, + } + ) return {"SPDisplaysDataType": controllers} @@ -46,8 +52,8 @@ def _make_subprocess_run_mock(sp_output): # ── _get_monitor_resolution_from_system_profiler ───────────────────────────── -class TestGetMonitorResolution: +class TestGetMonitorResolution: def test_pixelresolution_key(self): monitor = {"spdisplays_pixelresolution": "3024 x 1964 @ 120.00Hz"} result = _get_monitor_resolution_from_system_profiler(monitor) @@ -90,8 +96,8 @@ def test_no_digits_in_value_returns_none(self): # ── _get_refresh_rate_from_system_profiler ─────────────────────────────────── -class TestGetRefreshRate: +class TestGetRefreshRate: def test_refresh_rate_from_pixelresolution(self): monitor = {"spdisplays_pixelresolution": "3024 x 1964 @ 120.00Hz"} result = _get_refresh_rate_from_system_profiler(monitor) @@ -125,8 +131,8 @@ def test_refresh_rate_from_underscore_pixels_key(self): # ── _enrich_data_from_edid ─────────────────────────────────────────────────── -class TestEnrichDataFromEdid: +class TestEnrichDataFromEdid: def test_hex_prefix_stripped(self): """EDID strings starting with 0x should have the prefix removed.""" monitor = DisplayModuleInfo() @@ -153,21 +159,27 @@ def test_existing_fields_not_overwritten(self): # ── _fetch_monitor_info_system_profiler ────────────────────────────────────── -class TestFetchMonitorInfoSystemProfiler: +class TestFetchMonitorInfoSystemProfiler: @patch("hwprobe.core.mac.display.subprocess.run") def test_single_monitor_basic_info(self, mock_run): # gpu_name comes from the controller-level sppci_model, not the monitor dict - sp_data = {"SPDisplaysDataType": [{ - "_name": "Controller", - "sppci_model": "Apple M3 Pro", - "spdisplays_ndrvs": [{ - "_name": "Built-in Display", - "spdisplays_pixelresolution": "3024 x 1964 @ 120.00Hz", - "_spdisplays_display-serial-number": "SN123", - "_spdisplays_display-year": "2023", - }], - }]} + sp_data = { + "SPDisplaysDataType": [ + { + "_name": "Controller", + "sppci_model": "Apple M3 Pro", + "spdisplays_ndrvs": [ + { + "_name": "Built-in Display", + "spdisplays_pixelresolution": "3024 x 1964 @ 120.00Hz", + "_spdisplays_display-serial-number": "SN123", + "_spdisplays_display-year": "2023", + } + ], + } + ] + } mock_result = MagicMock() mock_result.stdout = json.dumps(sp_data) mock_run.return_value = mock_result @@ -186,9 +198,15 @@ def test_single_monitor_basic_info(self, mock_run): @patch("hwprobe.core.mac.display.subprocess.run") def test_missing_name_is_partial(self, mock_run): - sp_data = _make_sp_output([[{ - "spdisplays_pixelresolution": "1920 x 1080 @ 60Hz", - }]]) + sp_data = _make_sp_output( + [ + [ + { + "spdisplays_pixelresolution": "1920 x 1080 @ 60Hz", + } + ] + ] + ) mock_result = MagicMock() mock_result.stdout = json.dumps(sp_data) mock_run.return_value = mock_result @@ -200,10 +218,16 @@ def test_missing_name_is_partial(self, mock_run): @patch("hwprobe.core.mac.display.subprocess.run") def test_missing_serial_is_partial(self, mock_run): - sp_data = _make_sp_output([[{ - "_name": "Display", - "spdisplays_pixelresolution": "1920 x 1080 @ 60Hz", - }]]) + sp_data = _make_sp_output( + [ + [ + { + "_name": "Display", + "spdisplays_pixelresolution": "1920 x 1080 @ 60Hz", + } + ] + ] + ) mock_result = MagicMock() mock_result.stdout = json.dumps(sp_data) mock_run.return_value = mock_result @@ -214,11 +238,17 @@ def test_missing_serial_is_partial(self, mock_run): @patch("hwprobe.core.mac.display.subprocess.run") def test_missing_year_is_partial(self, mock_run): - sp_data = _make_sp_output([[{ - "_name": "Display", - "_spdisplays_display-serial-number": "SN1", - "spdisplays_pixelresolution": "1920 x 1080 @ 60Hz", - }]]) + sp_data = _make_sp_output( + [ + [ + { + "_name": "Display", + "_spdisplays_display-serial-number": "SN1", + "spdisplays_pixelresolution": "1920 x 1080 @ 60Hz", + } + ] + ] + ) mock_result = MagicMock() mock_result.stdout = json.dumps(sp_data) mock_run.return_value = mock_result @@ -228,14 +258,20 @@ def test_missing_year_is_partial(self, mock_run): @patch("hwprobe.core.mac.display.subprocess.run") def test_missing_gpu_name_is_partial(self, mock_run): - sp_data = {"SPDisplaysDataType": [{ - "spdisplays_ndrvs": [{ - "_name": "Display", - "spdisplays_pixelresolution": "1920 x 1080 @ 60Hz", - "_spdisplays_display-serial-number": "SN1", - "_spdisplays_display-year": "2023", - }] - }]} + sp_data = { + "SPDisplaysDataType": [ + { + "spdisplays_ndrvs": [ + { + "_name": "Display", + "spdisplays_pixelresolution": "1920 x 1080 @ 60Hz", + "_spdisplays_display-serial-number": "SN1", + "_spdisplays_display-year": "2023", + } + ] + } + ] + } mock_result = MagicMock() mock_result.stdout = json.dumps(sp_data) mock_run.return_value = mock_result @@ -263,10 +299,12 @@ def test_json_decode_error_returns_empty_list(self, mock_run): @patch("hwprobe.core.mac.display.subprocess.run") def test_multiple_monitors_across_controllers(self, mock_run): - sp_data = _make_sp_output([ - [{"_name": "Monitor A", "spdisplays_pixelresolution": "1920 x 1080 @ 60Hz"}], - [{"_name": "Monitor B", "spdisplays_pixelresolution": "2560 x 1440 @ 144Hz"}], - ]) + sp_data = _make_sp_output( + [ + [{"_name": "Monitor A", "spdisplays_pixelresolution": "1920 x 1080 @ 60Hz"}], + [{"_name": "Monitor B", "spdisplays_pixelresolution": "2560 x 1440 @ 144Hz"}], + ] + ) mock_result = MagicMock() mock_result.stdout = json.dumps(sp_data) mock_run.return_value = mock_result @@ -281,14 +319,20 @@ def test_edid_enrichment_called_when_present(self, mock_run): """When _spdisplays_edid is present, _enrich_data_from_edid is called.""" # 128 bytes of zeros is a minimal (invalid but parseable) EDID edid_hex = "00" * 128 - sp_data = _make_sp_output([[{ - "_name": "External Monitor", - "spdisplays_pixelresolution": "3840 x 2160 @ 60Hz", - "_spdisplays_display-serial-number": "SN1", - "_spdisplays_display-year": "2020", - "sppci_model": "AMD GPU", - "_spdisplays_edid": edid_hex, - }]]) + sp_data = _make_sp_output( + [ + [ + { + "_name": "External Monitor", + "spdisplays_pixelresolution": "3840 x 2160 @ 60Hz", + "_spdisplays_display-serial-number": "SN1", + "_spdisplays_display-year": "2020", + "sppci_model": "AMD GPU", + "_spdisplays_edid": edid_hex, + } + ] + ] + ) mock_result = MagicMock() mock_result.stdout = json.dumps(sp_data) mock_run.return_value = mock_result @@ -298,13 +342,19 @@ def test_edid_enrichment_called_when_present(self, mock_run): @patch("hwprobe.core.mac.display.subprocess.run") def test_missing_edid_is_partial(self, mock_run): - sp_data = _make_sp_output([[{ - "_name": "Display", - "spdisplays_pixelresolution": "1920 x 1080 @ 60Hz", - "_spdisplays_display-serial-number": "SN1", - "_spdisplays_display-year": "2023", - "sppci_model": "GPU", - }]]) + sp_data = _make_sp_output( + [ + [ + { + "_name": "Display", + "spdisplays_pixelresolution": "1920 x 1080 @ 60Hz", + "_spdisplays_display-serial-number": "SN1", + "_spdisplays_display-year": "2023", + "sppci_model": "GPU", + } + ] + ] + ) mock_result = MagicMock() mock_result.stdout = json.dumps(sp_data) mock_run.return_value = mock_result @@ -315,11 +365,12 @@ def test_missing_edid_is_partial(self, mock_run): # ── fetch_display_info ─────────────────────────────────────────────────────── -class TestFetchDisplayInfo: +class TestFetchDisplayInfo: @patch("hwprobe.core.mac.display.subprocess.run") def test_returns_display_info_type(self, mock_run): from hwprobe.models.display_models import DisplayInfo + sp_data = _make_sp_output() mock_result = MagicMock() mock_result.stdout = json.dumps(sp_data) @@ -330,10 +381,14 @@ def test_returns_display_info_type(self, mock_run): @patch("hwprobe.core.mac.display.subprocess.run") def test_modules_populated(self, mock_run): - sp_data = _make_sp_output([[ - {"_name": "A", "spdisplays_pixelresolution": "1920x1080 @ 60Hz"}, - {"_name": "B", "spdisplays_pixelresolution": "2560x1440 @ 120Hz"}, - ]]) + sp_data = _make_sp_output( + [ + [ + {"_name": "A", "spdisplays_pixelresolution": "1920x1080 @ 60Hz"}, + {"_name": "B", "spdisplays_pixelresolution": "2560x1440 @ 120Hz"}, + ] + ] + ) mock_result = MagicMock() mock_result.stdout = json.dumps(sp_data) mock_run.return_value = mock_result diff --git a/tests/core/mac/test_graphics.py b/tests/core/mac/test_graphics.py index f410a87..617a394 100644 --- a/tests/core/mac/test_graphics.py +++ b/tests/core/mac/test_graphics.py @@ -13,14 +13,14 @@ from dataclasses import dataclass from typing import Optional -from unittest.mock import patch, MagicMock +from unittest.mock import MagicMock, patch from hwprobe.core.mac.graphics import fetch_graphics_info from hwprobe.models.status_models import StatusType - # ── lightweight stand-ins for the binding's dataclasses ───────────────────── + @dataclass class FakeAppleGPUProperties: core_count: int @@ -43,14 +43,15 @@ class FakeGPUProperties: # ── helpers ────────────────────────────────────────────────────────────────── + def _apple_gpu( - name="Apple M3 Pro", - vendor_id=0x106B, - device_id=0x0000, - core_count=20, - gpu_perf_shaders=8, - gpu_gen=15, - unified_memory_mb=18432, + name="Apple M3 Pro", + vendor_id=0x106B, + device_id=0x0000, + core_count=20, + gpu_perf_shaders=8, + gpu_gen=15, + unified_memory_mb=18432, ) -> FakeGPUProperties: """Return a fully-populated Apple Silicon GPU stub.""" return FakeGPUProperties( @@ -68,10 +69,10 @@ def _apple_gpu( def _discrete_gpu( - name="NVIDIA GeForce RTX 3090", - vendor_id=0x10DE, - device_id=0x2204, - vram_mb=24576, + name="NVIDIA GeForce RTX 3090", + vendor_id=0x10DE, + device_id=0x2204, + vram_mb=24576, ) -> FakeGPUProperties: """Return a fully-populated discrete (non-Apple-Silicon) GPU stub.""" return FakeGPUProperties( @@ -101,6 +102,7 @@ def _patch_binding(gpu_list): # ── dylib / binding load failures ──────────────────────────────────────────── + class TestBindingLoadFailures: """fetch_graphics_info must gracefully handle errors that arise when the C binding cannot be imported or when IOKit enumeration fails.""" @@ -117,8 +119,8 @@ def test_dylib_not_found_returns_failed_status(self): raises FileNotFoundError whenever that specific module is imported, then evicting it from sys.modules so the lazy import is forced to run. """ - import sys import importlib.machinery + import sys _TARGET = "hwprobe.interops.mac.bindings.gpu_info" @@ -129,7 +131,6 @@ def find_spec(self, fullname, path, target=None): "libdevice_info.dylib not found at …/bindings/libdevice_info.dylib.\n" "Build the project first: cmake --build cmake-build-debug" ) - return None finder = _DylibMissingFinder() sys.meta_path.insert(0, finder) @@ -141,20 +142,16 @@ def find_spec(self, fullname, path, target=None): sys.modules.pop(_TARGET, None) assert info.status.type == StatusType.FAILED - assert any( - "libdevice_info.dylib" in m or "rebuild" in m.lower() - for m in info.status.messages - ) + assert any("libdevice_info.dylib" in m or "rebuild" in m.lower() for m in info.status.messages) assert info.modules == [] def test_iokit_enumeration_failure_returns_failed_status(self): """RuntimeError (get_gpu_info returns -1) → FAILED.""" mock_module = MagicMock() - mock_module.get_gpu_info.side_effect = RuntimeError( - "get_gpu_info() failed (C library returned -1)" - ) + mock_module.get_gpu_info.side_effect = RuntimeError("get_gpu_info() failed (C library returned -1)") import sys + sys.modules.pop("hwprobe.interops.mac.bindings.gpu_info", None) with patch.dict("sys.modules", {"hwprobe.interops.mac.bindings.gpu_info": mock_module}): @@ -170,6 +167,7 @@ def test_unexpected_exception_returns_failed_status(self): mock_module.get_gpu_info.side_effect = OSError("Unexpected OS error") import sys + sys.modules.pop("hwprobe.interops.mac.bindings.gpu_info", None) with patch.dict("sys.modules", {"hwprobe.interops.mac.bindings.gpu_info": mock_module}): @@ -182,11 +180,13 @@ def test_unexpected_exception_returns_failed_status(self): # ── Apple Silicon GPU (happy path) ─────────────────────────────────────────── + class TestAppleSiliconGPU: """Tests covering normal Apple Silicon GPU enumeration.""" def _run(self, gpu_list): import sys + sys.modules.pop("hwprobe.interops.mac.bindings.gpu_info", None) with _patch_binding(gpu_list): return fetch_graphics_info() @@ -231,7 +231,8 @@ def test_apple_silicon_nonzero_device_id_is_set(self): def test_apple_m1_gpu(self): info = self._run( - [_apple_gpu(name="Apple M1", core_count=7, gpu_perf_shaders=0, gpu_gen=13, unified_memory_mb=8192)]) + [_apple_gpu(name="Apple M1", core_count=7, gpu_perf_shaders=0, gpu_gen=13, unified_memory_mb=8192)] + ) gpu = info.modules[0] assert gpu.name == "Apple M1" assert gpu.vram.capacity == 8192 @@ -240,7 +241,8 @@ def test_apple_m1_gpu(self): def test_apple_m2_max_gpu(self): info = self._run( - [_apple_gpu(name="Apple M2 Max", core_count=38, gpu_perf_shaders=16, gpu_gen=14, unified_memory_mb=32768)]) + [_apple_gpu(name="Apple M2 Max", core_count=38, gpu_perf_shaders=16, gpu_gen=14, unified_memory_mb=32768)] + ) gpu = info.modules[0] assert gpu.name == "Apple M2 Max" assert gpu.vram.capacity == 32768 @@ -271,18 +273,21 @@ def test_no_gpus_returns_empty_success(self): # ── Discrete / non-Apple-Silicon GPU ───────────────────────────────────────── + class TestDiscreteGPU: """Tests for Intel, AMD, and NVIDIA GPUs on x86 Macs.""" def _run(self, gpu_list): import sys + sys.modules.pop("hwprobe.interops.mac.bindings.gpu_info", None) with _patch_binding(gpu_list): return fetch_graphics_info() def test_nvidia_gpu_success(self): info = self._run( - [_discrete_gpu(name="NVIDIA GeForce RTX 3090", vendor_id=0x10DE, device_id=0x2204, vram_mb=24576)]) + [_discrete_gpu(name="NVIDIA GeForce RTX 3090", vendor_id=0x10DE, device_id=0x2204, vram_mb=24576)] + ) assert info.status.type == StatusType.SUCCESS gpu = info.modules[0] assert gpu.name == "NVIDIA GeForce RTX 3090" @@ -358,21 +363,25 @@ def test_discrete_gpu_missing_vram_is_partial(self): # ── Multiple GPUs ───────────────────────────────────────────────────────────── + class TestMultipleGPUs: """Tests for machines with more than one GPU.""" def _run(self, gpu_list): import sys + sys.modules.pop("hwprobe.interops.mac.bindings.gpu_info", None) with _patch_binding(gpu_list): return fetch_graphics_info() def test_two_discrete_gpus(self): """Dual-GPU Intel Mac Pro style.""" - info = self._run([ - _discrete_gpu("AMD Radeon Pro W6800X", 0x1002, 0x73A3), - _discrete_gpu("AMD Radeon Pro W6800X Duo", 0x1002, 0x73A5), - ]) + info = self._run( + [ + _discrete_gpu("AMD Radeon Pro W6800X", 0x1002, 0x73A3), + _discrete_gpu("AMD Radeon Pro W6800X Duo", 0x1002, 0x73A5), + ] + ) assert info.status.type == StatusType.SUCCESS assert len(info.modules) == 2 assert info.modules[0].name == "AMD Radeon Pro W6800X" @@ -418,11 +427,13 @@ def test_all_modules_appended_even_with_partial_data(self): # ── Edge cases & name handling ──────────────────────────────────────────────── + class TestEdgeCases: """Misc edge-case and boundary tests.""" def _run(self, gpu_list): import sys + sys.modules.pop("hwprobe.interops.mac.bindings.gpu_info", None) with _patch_binding(gpu_list): return fetch_graphics_info() @@ -468,7 +479,9 @@ def test_zero_unified_memory_is_partial(self): device_id=0x0000, is_apple_silicon=True, apple_gpu=FakeAppleGPUProperties( - core_count=20, gpu_perf_shaders=8, gpu_gen=15, + core_count=20, + gpu_perf_shaders=8, + gpu_gen=15, unified_memory_mb=0, # bad value from binding ), ) @@ -507,5 +520,6 @@ def test_large_unified_memory(self): def test_return_type_is_graphics_info(self): from hwprobe.models.gpu_models import GraphicsInfo + info = self._run([_apple_gpu()]) assert isinstance(info, GraphicsInfo) diff --git a/tests/core/mac/test_memory.py b/tests/core/mac/test_memory.py index 181b570..04a7824 100644 --- a/tests/core/mac/test_memory.py +++ b/tests/core/mac/test_memory.py @@ -2,27 +2,26 @@ from unittest.mock import patch import pytest + from hwprobe.core.mac.memory import ( - get_ram_size_from_reg, + fetch_memory_info, get_arm_ram_info, + get_ram_size_from_reg, get_ram_size_from_system_profiler, - fetch_memory_info, ) from hwprobe.models.memory_models import MemoryInfo from hwprobe.models.status_models import StatusType - # ── get_ram_size_from_reg ──────────────────────────────────────────────────── -class TestGetRamSizeFromReg: +class TestGetRamSizeFromReg: def test_two_sticks_of_4gb(self): """ "02 00 00 00 00 00 00 00 02 00 00 00 00 00 00 00" Non-zero bytes: 0x02, 0x02 => 2 * 4096 = 8192 MB each. """ - reg = bytes([0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]) + reg = bytes([0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]) sizes = get_ram_size_from_reg(reg) assert len(sizes) == 2 assert all(s.capacity == 8192 for s in sizes) @@ -42,6 +41,7 @@ def test_single_nonzero_byte(self): def test_return_type_is_megabyte(self): from hwprobe.models.size_models import Megabyte + reg = bytes([0x01]) sizes = get_ram_size_from_reg(reg) assert isinstance(sizes[0], Megabyte) @@ -49,17 +49,21 @@ def test_return_type_is_megabyte(self): # ── get_arm_ram_info ───────────────────────────────────────────────────────── -class TestGetArmRamInfo: +class TestGetArmRamInfo: @patch("hwprobe.core.mac.memory.subprocess.check_output") def test_single_arm_module(self, mock_co): - plist_data = [{ - "_items": [{ - "SPMemoryDataType": "8 GB", - "dimm_manufacturer": "Samsung", - "dimm_type": "LPDDR5", - }] - }] + plist_data = [ + { + "_items": [ + { + "SPMemoryDataType": "8 GB", + "dimm_manufacturer": "Samsung", + "dimm_type": "LPDDR5", + } + ] + } + ] mock_co.return_value = plistlib.dumps(plist_data, fmt=plistlib.FMT_XML) info = get_arm_ram_info() @@ -86,18 +90,22 @@ def test_arm_ram_status_message_about_partial_data(self, mock_co): # ── get_ram_size_from_system_profiler ──────────────────────────────────────── -class TestGetRamSizeFromSystemProfiler: +class TestGetRamSizeFromSystemProfiler: @patch("hwprobe.core.mac.memory.subprocess.check_output") def test_two_dimms(self, mock_co): - plist_data = [{ - "_items": [{ + plist_data = [ + { "_items": [ - {"dimm_size": "8 GB"}, - {"dimm_size": "8 GB"}, + { + "_items": [ + {"dimm_size": "8 GB"}, + {"dimm_size": "8 GB"}, + ] + } ] - }] - }] + } + ] mock_co.return_value = plistlib.dumps(plist_data, fmt=plistlib.FMT_XML) sizes = get_ram_size_from_system_profiler() @@ -115,28 +123,37 @@ def test_failure_re_raises(self, mock_co): # ── fetch_memory_info (x86 path) ──────────────────────────────────────────── -class TestFetchMemoryInfoX86: +class TestFetchMemoryInfoX86: def _make_ioreg_plist(self, memory_entry): """Build a fake ioreg plist structure with the given memory dict.""" - return plistlib.dumps({ - "IORegistryEntryChildren": [{ - "IORegistryEntryChildren": [{ - "IORegistryEntryName": "memory", - **memory_entry, - }] - }] - }, fmt=plistlib.FMT_XML) + return plistlib.dumps( + { + "IORegistryEntryChildren": [ + { + "IORegistryEntryChildren": [ + { + "IORegistryEntryName": "memory", + **memory_entry, + } + ] + } + ] + }, + fmt=plistlib.FMT_XML, + ) @patch("hwprobe.core.mac.memory.get_ram_size_from_system_profiler") @patch("hwprobe.core.mac.memory.subprocess.check_output") def test_intel_two_dimms(self, mock_co, mock_sp): from hwprobe.models.size_models import Gigabyte + mock_sp.return_value = [Gigabyte(capacity=8), Gigabyte(capacity=8)] memory_entry = { - "reg": bytes([0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), + "reg": bytes( + [0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00] + ), "dimm-manufacturer": b"Samsung\x00Samsung\x00", "dimm-part-number": b"M471A1K43CB1-CTD\x00M471A1K43CB1-CTD\x00", "dimm-serial-number": b"ABCD1234\x00EFGH5678\x00", @@ -170,11 +187,15 @@ def side_effect(cmd): @patch("hwprobe.core.mac.memory.subprocess.check_output") def test_arm_detected_delegates_to_get_arm_ram_info(self, mock_co): """When uname returns arm64, fetch_memory_info should use get_arm_ram_info.""" - plist_data = [{ - "_items": [{ - "SPMemoryDataType": "16 GB", - }] - }] + plist_data = [ + { + "_items": [ + { + "SPMemoryDataType": "16 GB", + } + ] + } + ] def side_effect(cmd): if cmd == ["uname", "-m"]: @@ -214,14 +235,21 @@ def test_system_profiler_failure_falls_back_to_reg(self, mock_co, mock_sp): "dimm-types": b"DDR4\x00", "slot-names": b"DIMM0/BANK0\x00", } - ioreg_plist = plistlib.dumps({ - "IORegistryEntryChildren": [{ - "IORegistryEntryChildren": [{ - "IORegistryEntryName": "memory", - **memory_entry, - }] - }] - }, fmt=plistlib.FMT_XML) + ioreg_plist = plistlib.dumps( + { + "IORegistryEntryChildren": [ + { + "IORegistryEntryChildren": [ + { + "IORegistryEntryName": "memory", + **memory_entry, + } + ] + } + ] + }, + fmt=plistlib.FMT_XML, + ) def side_effect(cmd): if cmd == ["uname", "-m"]: @@ -245,14 +273,21 @@ def test_return_type_is_memory_info(self, mock_co, mock_sp): memory_entry = { "reg": b"\x00" * 16, } - ioreg_plist = plistlib.dumps({ - "IORegistryEntryChildren": [{ - "IORegistryEntryChildren": [{ - "IORegistryEntryName": "memory", - **memory_entry, - }] - }] - }, fmt=plistlib.FMT_XML) + ioreg_plist = plistlib.dumps( + { + "IORegistryEntryChildren": [ + { + "IORegistryEntryChildren": [ + { + "IORegistryEntryName": "memory", + **memory_entry, + } + ] + } + ] + }, + fmt=plistlib.FMT_XML, + ) def side_effect(cmd): if cmd == ["uname", "-m"]: @@ -269,12 +304,13 @@ def side_effect(cmd): # ── BUG: ecc_enabled is always False ───────────────────────────────────────── -class TestECCDetection: +class TestECCDetection: @patch("hwprobe.core.mac.memory.get_ram_size_from_system_profiler") @patch("hwprobe.core.mac.memory.subprocess.check_output") def test_ecc_enabled_detected(self, mock_co, mock_sp): from hwprobe.models.size_models import Gigabyte + mock_sp.return_value = [Gigabyte(capacity=32)] memory_entry = { @@ -287,14 +323,21 @@ def test_ecc_enabled_detected(self, mock_co, mock_sp): "ecc-enabled": True, "slot-names": b"DIMM0/BANK0\x00", } - ioreg_plist = plistlib.dumps({ - "IORegistryEntryChildren": [{ - "IORegistryEntryChildren": [{ - "IORegistryEntryName": "memory", - **memory_entry, - }] - }] - }, fmt=plistlib.FMT_XML) + ioreg_plist = plistlib.dumps( + { + "IORegistryEntryChildren": [ + { + "IORegistryEntryChildren": [ + { + "IORegistryEntryName": "memory", + **memory_entry, + } + ] + } + ] + }, + fmt=plistlib.FMT_XML, + ) def side_effect(cmd): if cmd == ["uname", "-m"]: @@ -312,6 +355,7 @@ def side_effect(cmd): # ── BUG: Uneven list lengths cause IndexError ──────────────────────────────── + class TestUnevenListLengths: """When lists have different lengths, shorter ones should be skipped gracefully and both modules should still be appended.""" @@ -320,6 +364,7 @@ class TestUnevenListLengths: @patch("hwprobe.core.mac.memory.subprocess.check_output") def test_mismatched_list_lengths_still_appends_both(self, mock_co, mock_sp): from hwprobe.models.size_models import Gigabyte + mock_sp.return_value = [Gigabyte(capacity=8)] # 2 manufacturers but only 1 of everything else @@ -332,14 +377,21 @@ def test_mismatched_list_lengths_still_appends_both(self, mock_co, mock_sp): "dimm-types": b"DDR4\x00", "slot-names": b"DIMM0/BANK0\x00", } - ioreg_plist = plistlib.dumps({ - "IORegistryEntryChildren": [{ - "IORegistryEntryChildren": [{ - "IORegistryEntryName": "memory", - **memory_entry, - }] - }] - }, fmt=plistlib.FMT_XML) + ioreg_plist = plistlib.dumps( + { + "IORegistryEntryChildren": [ + { + "IORegistryEntryChildren": [ + { + "IORegistryEntryName": "memory", + **memory_entry, + } + ] + } + ] + }, + fmt=plistlib.FMT_XML, + ) def side_effect(cmd): if cmd == ["uname", "-m"]: diff --git a/tests/core/mac/test_network.py b/tests/core/mac/test_network.py index edc25c7..bbbc4fe 100644 --- a/tests/core/mac/test_network.py +++ b/tests/core/mac/test_network.py @@ -1,11 +1,12 @@ import plistlib -from unittest.mock import patch, MagicMock +from unittest.mock import MagicMock, patch import pytest + from hwprobe.core.mac.network import ( + _fetch_airport_details, _fetch_controllers, _fetch_ethernet_details, - _fetch_airport_details, _fetch_system_profiler_details, _find_child, _get_bsd_interface_apple_silicon, @@ -13,9 +14,9 @@ ) from hwprobe.models.network_models import NetworkInfo, NICInfo - # ── helpers ────────────────────────────────────────────────────────────────── + def _make_subprocess_result(stdout_str="", stdout_bytes=None): mock_result = MagicMock() if stdout_bytes is not None: @@ -41,9 +42,9 @@ def _make_ioreg_plist(items): def _make_intel_ioreg_entry( - io_name_matched="pci14e4,4331", - io_model="AirPort Extreme", - bsd_name="en1", + io_name_matched="pci14e4,4331", + io_model="AirPort Extreme", + bsd_name="en1", ): """AirPort_BrcmNIC entry as seen on Intel Macs.""" return { @@ -60,9 +61,9 @@ def _make_intel_ioreg_entry( def _make_apple_silicon_ioreg_entry( - manufacturer_id=0x14e4, - product_id=0x4488, - bsd_name="en0", + manufacturer_id=0x14E4, + product_id=0x4488, + bsd_name="en0", ): """AppleBCMWLANCore entry as seen on Apple Silicon Macs.""" return { @@ -91,9 +92,9 @@ def _make_apple_silicon_ioreg_entry( def _make_brcm4331_ioreg_entry( - io_name_matched="pci14e4,4331", - io_model="Wireless Network Adapter (802.11 a/b/g/n)", - bsd_name="en1", + io_name_matched="pci14e4,4331", + io_model="Wireless Network Adapter (802.11 a/b/g/n)", + bsd_name="en1", ): """AirPort_Brcm4331 entry as seen on older Intel Macs.""" return { @@ -111,8 +112,8 @@ def _make_brcm4331_ioreg_entry( # ── _fetch_controllers ─────────────────────────────────────────────────────── -class TestFetchControllers: +class TestFetchControllers: @patch("hwprobe.core.mac.network.subprocess.run") def test_returns_interface_list(self, mock_run): mock_run.return_value = _make_subprocess_result("en0 en1 en2") @@ -143,16 +144,20 @@ def test_subprocess_failure_propagates(self, mock_run): # ── _fetch_ethernet_details ────────────────────────────────────────────────── -class TestFetchEthernetDetails: +class TestFetchEthernetDetails: @patch("hwprobe.core.mac.network.subprocess.run") def test_single_ethernet_controller(self, mock_run): - plist_data = _make_ethernet_plist([{ - "spethernet_BSD_Device_Name": "en0", - "spethernet_vendor-id": "0x8086", - "spethernet_vendor_name": "Intel", - "spethernet_product-id": "0x15B8", - }]) + plist_data = _make_ethernet_plist( + [ + { + "spethernet_BSD_Device_Name": "en0", + "spethernet_vendor-id": "0x8086", + "spethernet_vendor_name": "Intel", + "spethernet_product-id": "0x15B8", + } + ] + ) mock_run.return_value = MagicMock(stdout=plist_data) result = _fetch_ethernet_details() @@ -163,20 +168,22 @@ def test_single_ethernet_controller(self, mock_run): @patch("hwprobe.core.mac.network.subprocess.run") def test_multiple_ethernet_controllers(self, mock_run): - plist_data = _make_ethernet_plist([ - { - "spethernet_BSD_Device_Name": "en0", - "spethernet_vendor-id": "0x8086", - "spethernet_vendor_name": "Intel", - "spethernet_product-id": "0x15B8", - }, - { - "spethernet_BSD_Device_Name": "en3", - "spethernet_vendor-id": "0x14e4", - "spethernet_vendor_name": "Broadcom", - "spethernet_product-id": "0x1682", - }, - ]) + plist_data = _make_ethernet_plist( + [ + { + "spethernet_BSD_Device_Name": "en0", + "spethernet_vendor-id": "0x8086", + "spethernet_vendor_name": "Intel", + "spethernet_product-id": "0x15B8", + }, + { + "spethernet_BSD_Device_Name": "en3", + "spethernet_vendor-id": "0x14e4", + "spethernet_vendor_name": "Broadcom", + "spethernet_product-id": "0x1682", + }, + ] + ) mock_run.return_value = MagicMock(stdout=plist_data) result = _fetch_ethernet_details() @@ -194,8 +201,8 @@ def test_subprocess_failure_propagates(self, mock_run): # ── _find_child ────────────────────────────────────────────────────────────── -class TestFindChild: +class TestFindChild: def test_returns_matching_dict(self): children = [ {"IOObjectClass": "Foo"}, @@ -228,8 +235,8 @@ def test_returns_first_match(self): # ── _get_bsd_interface_apple_silicon ───────────────────────────────────────── -class TestGetBsdInterfaceAppleSilicon: +class TestGetBsdInterfaceAppleSilicon: def _make_item(self, bsd_name="en0"): return _make_apple_silicon_ioreg_entry(bsd_name=bsd_name) @@ -274,16 +281,20 @@ def test_returns_none_when_children_key_absent(self): # ── _fetch_airport_details ─────────────────────────────────────────────────── -class TestFetchAirportDetails: +class TestFetchAirportDetails: @patch("hwprobe.core.mac.network.subprocess.run") def test_intel_mac_brcm_nic(self, mock_run): """AirPort_BrcmNIC entry is parsed correctly on Intel Macs.""" - plist_data = _make_ioreg_plist([_make_intel_ioreg_entry( - io_name_matched="pci14e4,4331", - io_model="AirPort Extreme", - bsd_name="en1", - )]) + plist_data = _make_ioreg_plist( + [ + _make_intel_ioreg_entry( + io_name_matched="pci14e4,4331", + io_model="AirPort Extreme", + bsd_name="en1", + ) + ] + ) mock_run.return_value = MagicMock(stdout=plist_data) result = _fetch_airport_details() @@ -295,9 +306,13 @@ def test_intel_mac_brcm_nic(self, mock_run): @patch("hwprobe.core.mac.network.subprocess.run") def test_intel_mac_vendor_device_uppercased(self, mock_run): """Vendor and device IDs are stored as uppercase hex strings.""" - plist_data = _make_ioreg_plist([_make_intel_ioreg_entry( - io_name_matched="pci8086,095a", - )]) + plist_data = _make_ioreg_plist( + [ + _make_intel_ioreg_entry( + io_name_matched="pci8086,095a", + ) + ] + ) mock_run.return_value = MagicMock(stdout=plist_data) result = _fetch_airport_details() @@ -322,11 +337,15 @@ def test_intel_mac_no_matching_interface_child(self, mock_run): @patch("hwprobe.core.mac.network.subprocess.run") def test_apple_silicon_bcm_wlan_core(self, mock_run): """AppleBCMWLANCore entry is parsed correctly on Apple Silicon Macs.""" - plist_data = _make_ioreg_plist([_make_apple_silicon_ioreg_entry( - manufacturer_id=0x14e4, - product_id=0x4488, - bsd_name="en0", - )]) + plist_data = _make_ioreg_plist( + [ + _make_apple_silicon_ioreg_entry( + manufacturer_id=0x14E4, + product_id=0x4488, + bsd_name="en0", + ) + ] + ) mock_run.return_value = MagicMock(stdout=plist_data) result = _fetch_airport_details() @@ -340,7 +359,7 @@ def test_apple_silicon_no_bsd_interface_skipped(self, mock_run): """Apple Silicon entry with no resolvable BSD interface is not added.""" entry = { "IORegistryEntryName": "AppleBCMWLANCore", - "ModuleDictionary": {"ManufacturerID": 0x14e4, "ProductID": 0x4488}, + "ModuleDictionary": {"ManufacturerID": 0x14E4, "ProductID": 0x4488}, "IORegistryEntryChildren": [], # missing Skywalk tree } plist_data = _make_ioreg_plist([entry]) @@ -352,11 +371,15 @@ def test_apple_silicon_no_bsd_interface_skipped(self, mock_run): @patch("hwprobe.core.mac.network.subprocess.run") def test_brcm4331_driver(self, mock_run): """AirPort_Brcm4331 entry is parsed correctly on older Intel Macs.""" - plist_data = _make_ioreg_plist([_make_brcm4331_ioreg_entry( - io_name_matched="pci14e4,4331", - io_model="Wireless Network Adapter (802.11 a/b/g/n)", - bsd_name="en1", - )]) + plist_data = _make_ioreg_plist( + [ + _make_brcm4331_ioreg_entry( + io_name_matched="pci14e4,4331", + io_model="Wireless Network Adapter (802.11 a/b/g/n)", + bsd_name="en1", + ) + ] + ) mock_run.return_value = MagicMock(stdout=plist_data) result = _fetch_airport_details() @@ -368,10 +391,14 @@ def test_brcm4331_driver(self, mock_run): @patch("hwprobe.core.mac.network.subprocess.run") def test_brcm4331_alternate_device_id(self, mock_run): """AirPort_Brcm4331 supports multiple device IDs (4331, 4353, 432b).""" - plist_data = _make_ioreg_plist([_make_brcm4331_ioreg_entry( - io_name_matched="pci14e4,4353", - bsd_name="en1", - )]) + plist_data = _make_ioreg_plist( + [ + _make_brcm4331_ioreg_entry( + io_name_matched="pci14e4,4353", + bsd_name="en1", + ) + ] + ) mock_run.return_value = MagicMock(stdout=plist_data) result = _fetch_airport_details() @@ -415,9 +442,7 @@ def test_brcm4331_no_regex_match(self, mock_run): "IORegistryEntryName": "AirPort_Brcm4331", "IONameMatched": "invalid-format", "IOModel": "Wireless Network Adapter", - "IORegistryEntryChildren": [ - {"IOObjectClass": "en1", "IORegistryEntryName": "en1"} - ], + "IORegistryEntryChildren": [{"IOObjectClass": "en1", "IORegistryEntryName": "en1"}], } plist_data = _make_ioreg_plist([entry]) mock_run.return_value = MagicMock(stdout=plist_data) @@ -447,10 +472,12 @@ def test_missing_driver_name_returns_early(self, mock_run): @patch("hwprobe.core.mac.network.subprocess.run") def test_multiple_controllers(self, mock_run): """Multiple controllers across both Mac types are all collected.""" - plist_data = _make_ioreg_plist([ - _make_intel_ioreg_entry(bsd_name="en1"), - _make_apple_silicon_ioreg_entry(bsd_name="en0"), - ]) + plist_data = _make_ioreg_plist( + [ + _make_intel_ioreg_entry(bsd_name="en1"), + _make_apple_silicon_ioreg_entry(bsd_name="en0"), + ] + ) mock_run.return_value = MagicMock(stdout=plist_data) result = _fetch_airport_details() @@ -466,22 +493,24 @@ def test_subprocess_failure_propagates(self, mock_run): # ── _fetch_system_profiler_details ─────────────────────────────────────────── -class TestFetchSystemProfilerDetails: +class TestFetchSystemProfilerDetails: @patch("hwprobe.core.mac.network._fetch_ethernet_details") @patch("hwprobe.core.mac.network.subprocess.run") def test_single_ethernet_nic(self, mock_run, mock_eth): - network_plist = _make_network_plist([{ - "interface": "en0", - "_name": "Ethernet", - "Ethernet": {"MAC Address": "aa:bb:cc:dd:ee:ff"}, - "type": "Ethernet", - "IPv4": {"Addresses": ["192.168.1.100"]}, - }]) + network_plist = _make_network_plist( + [ + { + "interface": "en0", + "_name": "Ethernet", + "Ethernet": {"MAC Address": "aa:bb:cc:dd:ee:ff"}, + "type": "Ethernet", + "IPv4": {"Addresses": ["192.168.1.100"]}, + } + ] + ) mock_run.return_value = MagicMock(stdout=network_plist) - mock_eth.return_value = { - "en0": NICInfo(vendor_id="0x8086", manufacturer="Intel", device_id="0x15B8") - } + mock_eth.return_value = {"en0": NICInfo(vendor_id="0x8086", manufacturer="Intel", device_id="0x15B8")} result = _fetch_system_profiler_details(["en0"]) assert len(result.modules) == 1 @@ -497,16 +526,18 @@ def test_single_ethernet_nic(self, mock_run, mock_eth): @patch("hwprobe.core.mac.network._fetch_airport_details") @patch("hwprobe.core.mac.network.subprocess.run") def test_single_wifi_nic(self, mock_run, mock_air): - network_plist = _make_network_plist([{ - "interface": "en1", - "_name": "Wi-Fi", - "Ethernet": {"MAC Address": "11:22:33:44:55:66"}, - "type": "AirPort", - }]) + network_plist = _make_network_plist( + [ + { + "interface": "en1", + "_name": "Wi-Fi", + "Ethernet": {"MAC Address": "11:22:33:44:55:66"}, + "type": "AirPort", + } + ] + ) mock_run.return_value = MagicMock(stdout=network_plist) - mock_air.return_value = { - "en1": NICInfo(vendor_id="0x14e4", device_id="0x4331") - } + mock_air.return_value = {"en1": NICInfo(vendor_id="0x14e4", device_id="0x4331")} result = _fetch_system_profiler_details(["en1"]) assert len(result.modules) == 1 @@ -516,12 +547,16 @@ def test_single_wifi_nic(self, mock_run, mock_air): @patch("hwprobe.core.mac.network.subprocess.run") def test_interface_not_in_valid_list_is_skipped(self, mock_run): - network_plist = _make_network_plist([{ - "interface": "en5", - "_name": "USB Ethernet", - "Ethernet": {"MAC Address": "aa:bb:cc:dd:ee:ff"}, - "type": "Ethernet", - }]) + network_plist = _make_network_plist( + [ + { + "interface": "en5", + "_name": "USB Ethernet", + "Ethernet": {"MAC Address": "aa:bb:cc:dd:ee:ff"}, + "type": "Ethernet", + } + ] + ) mock_run.return_value = MagicMock(stdout=network_plist) result = _fetch_system_profiler_details(["en0", "en1"]) @@ -530,11 +565,15 @@ def test_interface_not_in_valid_list_is_skipped(self, mock_run): @patch("hwprobe.core.mac.network.subprocess.run") def test_no_mac_address_skipped(self, mock_run): """Devices without MAC address are skipped (unplugged devices).""" - network_plist = _make_network_plist([{ - "interface": "en0", - "_name": "Ethernet", - "type": "Ethernet", - }]) + network_plist = _make_network_plist( + [ + { + "interface": "en0", + "_name": "Ethernet", + "type": "Ethernet", + } + ] + ) mock_run.return_value = MagicMock(stdout=network_plist) result = _fetch_system_profiler_details(["en0"]) @@ -542,12 +581,16 @@ def test_no_mac_address_skipped(self, mock_run): @patch("hwprobe.core.mac.network.subprocess.run") def test_no_ip_address_still_included(self, mock_run): - network_plist = _make_network_plist([{ - "interface": "en0", - "_name": "Ethernet", - "Ethernet": {"MAC Address": "aa:bb:cc:dd:ee:ff"}, - "type": "Ethernet", - }]) + network_plist = _make_network_plist( + [ + { + "interface": "en0", + "_name": "Ethernet", + "Ethernet": {"MAC Address": "aa:bb:cc:dd:ee:ff"}, + "type": "Ethernet", + } + ] + ) mock_run.return_value = MagicMock(stdout=network_plist) result = _fetch_system_profiler_details(["en0"]) @@ -558,20 +601,22 @@ def test_no_ip_address_still_included(self, mock_run): @patch("hwprobe.core.mac.network._fetch_airport_details") @patch("hwprobe.core.mac.network.subprocess.run") def test_mixed_ethernet_and_wifi(self, mock_run, mock_air, mock_eth): - network_plist = _make_network_plist([ - { - "interface": "en0", - "_name": "Ethernet", - "Ethernet": {"MAC Address": "aa:bb:cc:dd:ee:ff"}, - "type": "Ethernet", - }, - { - "interface": "en1", - "_name": "Wi-Fi", - "Ethernet": {"MAC Address": "11:22:33:44:55:66"}, - "type": "AirPort", - }, - ]) + network_plist = _make_network_plist( + [ + { + "interface": "en0", + "_name": "Ethernet", + "Ethernet": {"MAC Address": "aa:bb:cc:dd:ee:ff"}, + "type": "Ethernet", + }, + { + "interface": "en1", + "_name": "Wi-Fi", + "Ethernet": {"MAC Address": "11:22:33:44:55:66"}, + "type": "AirPort", + }, + ] + ) mock_run.return_value = MagicMock(stdout=network_plist) mock_eth.return_value = {"en0": NICInfo(vendor_id="0x8086")} mock_air.return_value = {"en1": NICInfo(vendor_id="0x14e4")} @@ -582,13 +627,11 @@ def test_mixed_ethernet_and_wifi(self, mock_run, mock_air, mock_eth): # ── Missing _items key handled gracefully ───────────────────────────────── -class TestMissingItemsKey: +class TestMissingItemsKey: @patch("hwprobe.core.mac.network.subprocess.run") def test_missing_items_key_returns_empty(self, mock_run): - bad_plist = plistlib.dumps([{ - "not_items": [{"interface": "en0"}] - }], fmt=plistlib.FMT_XML) + bad_plist = plistlib.dumps([{"not_items": [{"interface": "en0"}]}], fmt=plistlib.FMT_XML) mock_run.return_value = MagicMock(stdout=bad_plist) result = _fetch_system_profiler_details(["en0"]) @@ -597,8 +640,8 @@ def test_missing_items_key_returns_empty(self, mock_run): # ── Empty controller list ───────────────────────────────────────────────── -class TestEmptyControllers: +class TestEmptyControllers: @patch("hwprobe.core.mac.network.subprocess.run") def test_empty_controllers_passes_empty_list(self, mock_run): def side_effect(cmd, **kwargs): @@ -617,8 +660,8 @@ def side_effect(cmd, **kwargs): # ── fetch_network_info ─────────────────────────────────────────────────────── -class TestFetchNetworkInfo: +class TestFetchNetworkInfo: @patch("hwprobe.core.mac.network.subprocess.run") def test_returns_network_info_type(self, mock_run): def side_effect(cmd, **kwargs): diff --git a/tests/core/mac/test_storage.py b/tests/core/mac/test_storage.py index 0ad18ef..13728f8 100644 --- a/tests/core/mac/test_storage.py +++ b/tests/core/mac/test_storage.py @@ -1,12 +1,12 @@ from dataclasses import dataclass -from unittest.mock import patch, MagicMock +from unittest.mock import MagicMock, patch from hwprobe.core.mac.storage import fetch_storage_info from hwprobe.models.status_models import StatusType - # ── lightweight stand-in for the binding's dataclass ───────────────────────── + @dataclass class FakeStorageDeviceProperties: product_name: str @@ -20,14 +20,15 @@ class FakeStorageDeviceProperties: # ── helpers ────────────────────────────────────────────────────────────────── + def _nvme_ssd( - product_name="APPLE SSD AP0512Z", - vendor_name="", - medium_type="Solid State", - interconnect="PCI-Express", - location="Internal", - bsd_name="disk0", - size_bytes=500_107_862_016, + product_name="APPLE SSD AP0512Z", + vendor_name="", + medium_type="Solid State", + interconnect="PCI-Express", + location="Internal", + bsd_name="disk0", + size_bytes=500_107_862_016, ) -> FakeStorageDeviceProperties: return FakeStorageDeviceProperties( product_name=product_name, @@ -41,13 +42,13 @@ def _nvme_ssd( def _sata_ssd( - product_name="Samsung SSD 860 EVO 1TB", - vendor_name="Samsung", - medium_type="Solid State", - interconnect="SATA", - location="Internal", - bsd_name="disk1", - size_bytes=1_000_204_886_016, + product_name="Samsung SSD 860 EVO 1TB", + vendor_name="Samsung", + medium_type="Solid State", + interconnect="SATA", + location="Internal", + bsd_name="disk1", + size_bytes=1_000_204_886_016, ) -> FakeStorageDeviceProperties: return FakeStorageDeviceProperties( product_name=product_name, @@ -61,13 +62,13 @@ def _sata_ssd( def _hdd( - product_name="WDC WD10EZEX-00W", - vendor_name="Western Digital", - medium_type="Rotational", - interconnect="SATA", - location="Internal", - bsd_name="disk2", - size_bytes=1_000_204_886_016, + product_name="WDC WD10EZEX-00W", + vendor_name="Western Digital", + medium_type="Rotational", + interconnect="SATA", + location="Internal", + bsd_name="disk2", + size_bytes=1_000_204_886_016, ) -> FakeStorageDeviceProperties: return FakeStorageDeviceProperties( product_name=product_name, @@ -81,13 +82,13 @@ def _hdd( def _usb_drive( - product_name="SanDisk Ultra", - vendor_name="SanDisk", - medium_type="", - interconnect="USB", - location="External", - bsd_name="disk3", - size_bytes=32_015_982_592, + product_name="SanDisk Ultra", + vendor_name="SanDisk", + medium_type="", + interconnect="USB", + location="External", + bsd_name="disk3", + size_bytes=32_015_982_592, ) -> FakeStorageDeviceProperties: return FakeStorageDeviceProperties( product_name=product_name, @@ -101,13 +102,13 @@ def _usb_drive( def _apple_fabric_ssd( - product_name="APPLE SSD AP0512Z", - vendor_name="", - medium_type="Solid State", - interconnect="Apple Fabric", - location="Internal", - bsd_name="disk0", - size_bytes=500_107_862_016, + product_name="APPLE SSD AP0512Z", + vendor_name="", + medium_type="Solid State", + interconnect="Apple Fabric", + location="Internal", + bsd_name="disk0", + size_bytes=500_107_862_016, ) -> FakeStorageDeviceProperties: return FakeStorageDeviceProperties( product_name=product_name, @@ -137,6 +138,7 @@ def _patch_binding(disk_list): def _run(disk_list): import sys + sys.modules.pop("hwprobe.interops.mac.bindings.storage_info", None) with _patch_binding(disk_list): return fetch_storage_info() @@ -144,14 +146,15 @@ def _run(disk_list): # ── dylib / binding load failures ──────────────────────────────────────────── + class TestBindingLoadFailures: """fetch_storage_info must gracefully handle errors that arise when the C binding cannot be imported or when IOKit enumeration fails.""" def test_dylib_not_found_returns_failed_status(self): """FileNotFoundError raised at module import time -> FAILED with a rebuild hint.""" - import sys import importlib.abc + import sys _TARGET = "hwprobe.interops.mac.bindings.storage_info" @@ -162,7 +165,6 @@ def find_spec(self, fullname, path, target=None): "libdevice_info.dylib not found at .../bindings/libdevice_info.dylib.\n" "Build the project first: cmake --build cmake-build-debug" ) - return None finder = _DylibMissingFinder() sys.meta_path.insert(0, finder) @@ -174,20 +176,16 @@ def find_spec(self, fullname, path, target=None): sys.modules.pop(_TARGET, None) assert info.status.type == StatusType.FAILED - assert any( - "libdevice_info.dylib" in m or "rebuild" in m.lower() - for m in info.status.messages - ) + assert any("libdevice_info.dylib" in m or "rebuild" in m.lower() for m in info.status.messages) assert info.modules == [] def test_iokit_enumeration_failure_returns_failed_status(self): """RuntimeError (get_storage_info returns -1) -> FAILED.""" mock_module = MagicMock() - mock_module.get_storage_info.side_effect = RuntimeError( - "get_storage_info() failed (C library returned -1)" - ) + mock_module.get_storage_info.side_effect = RuntimeError("get_storage_info() failed (C library returned -1)") import sys + sys.modules.pop("hwprobe.interops.mac.bindings.storage_info", None) with patch.dict("sys.modules", {"hwprobe.interops.mac.bindings.storage_info": mock_module}): @@ -203,6 +201,7 @@ def test_unexpected_exception_returns_failed_status(self): mock_module.get_storage_info.side_effect = OSError("Unexpected OS error") import sys + sys.modules.pop("hwprobe.interops.mac.bindings.storage_info", None) with patch.dict("sys.modules", {"hwprobe.interops.mac.bindings.storage_info": mock_module}): @@ -215,6 +214,7 @@ def test_unexpected_exception_returns_failed_status(self): # ── NVMe SSD (happy path) ──────────────────────────────────────────────────── + class TestNVMeSSD: """Tests covering NVMe SSD enumeration via PCI-Express interconnect.""" @@ -258,6 +258,7 @@ def test_nvme_pci_express_case_insensitive(self): # ── Apple Fabric SSD ───────────────────────────────────────────────────────── + class TestAppleFabricSSD: """Tests for Apple Silicon Macs using Apple Fabric interconnect.""" @@ -281,6 +282,7 @@ def test_apple_fabric_ssd_vendor_name_takes_priority(self): # ── SATA SSD ───────────────────────────────────────────────────────────────── + class TestSATASSD: """Tests for SATA-connected SSDs.""" @@ -307,6 +309,7 @@ def test_sata_ssd_size(self): # ── HDD ────────────────────────────────────────────────────────────────────── + class TestHDD: """Tests for rotational hard disk drives.""" @@ -322,6 +325,7 @@ def test_hdd_manufacturer(self): # ── USB / External Drives ──────────────────────────────────────────────────── + class TestExternalDrive: """Tests for USB and other external drives.""" @@ -344,6 +348,7 @@ def test_usb_drive_size(self): # ── Multiple Disks ─────────────────────────────────────────────────────────── + class TestMultipleDisks: """Tests for machines with more than one storage device.""" @@ -353,21 +358,25 @@ def test_two_disks_enumerated(self): assert len(info.modules) == 2 def test_multiple_disk_types_correct(self): - info = _run([ - _nvme_ssd(product_name="NVMe Drive"), - _sata_ssd(product_name="SATA SSD"), - _hdd(product_name="Big HDD"), - ]) + info = _run( + [ + _nvme_ssd(product_name="NVMe Drive"), + _sata_ssd(product_name="SATA SSD"), + _hdd(product_name="Big HDD"), + ] + ) assert len(info.modules) == 3 assert info.modules[0].type == "Non-Volatile Memory Express (NVMe)" assert info.modules[1].type == "Solid State Drive (SSD)" assert info.modules[2].type == "Hard Disk Drive (HDD)" def test_internal_and_external_mixed(self): - info = _run([ - _apple_fabric_ssd(location="Internal"), - _usb_drive(location="External"), - ]) + info = _run( + [ + _apple_fabric_ssd(location="Internal"), + _usb_drive(location="External"), + ] + ) assert len(info.modules) == 2 assert info.modules[0].location == "Internal" assert info.modules[1].location == "External" @@ -375,6 +384,7 @@ def test_internal_and_external_mixed(self): # ── Edge Cases ─────────────────────────────────────────────────────────────── + class TestEdgeCases: """Misc edge-case and boundary tests.""" @@ -434,6 +444,7 @@ def test_large_disk_size(self): def test_return_type_is_storage_info(self): from hwprobe.models.storage_models import StorageInfo + info = _run([_nvme_ssd()]) assert isinstance(info, StorageInfo) diff --git a/tests/core/windows/test_display.py b/tests/core/windows/test_display.py index 186e129..f5873c9 100644 --- a/tests/core/windows/test_display.py +++ b/tests/core/windows/test_display.py @@ -1,19 +1,19 @@ import ctypes import struct -from ctypes import py_object, addressof +from ctypes import addressof, py_object -import hwprobe.core.windows.display as display import pytest + +from hwprobe.core.windows import display from hwprobe.interops.win.legacy.constants import ( - STATUS_OK, - STATUS_NOK, - STATUS_INVALID_ARG, STATUS_FAILURE, + STATUS_INVALID_ARG, + STATUS_NOK, + STATUS_OK, ) from hwprobe.models.display_models import DisplayInfo from hwprobe.models.status_models import StatusType - # ============================================================ # Helpers # ============================================================ @@ -36,9 +36,9 @@ def build_minimal_edid(name=b"TEST-MONITOR", width_cm=60, height_cm=34): edid[21] = width_cm edid[22] = height_cm off = 54 - edid[off: off + 4] = b"\x00\x00\x00\xfc" + edid[off : off + 4] = b"\x00\x00\x00\xfc" edid[off + 4] = 0x00 - edid[off + 5: off + 5 + len(name)] = name + edid[off + 5 : off + 5 + len(name)] = name edid[off + 5 + len(name)] = 0x0A return bytes(edid), vendor @@ -49,7 +49,6 @@ def build_minimal_edid(name=b"TEST-MONITOR", width_cm=60, height_cm=34): class TestAspectRatios: - @pytest.mark.parametrize( "width,height,real,friendly", [ @@ -86,7 +85,6 @@ def test_invalid_dimensions(self, width, height): class TestEDIDParsing: - def test_minimal_edid(self): edid, vendor = build_minimal_edid() parsed = display.parse_edid(edid) @@ -113,21 +111,15 @@ def test_invalid_hdev(self, monkeypatch): def mockfail_SetupDiGetClassDevsA(cGuidPtr, enumerator, hwndParent, flags): return -1 # simulate failure - monkeypatch.setattr( - display, "SetupDiGetClassDevsA", mockfail_SetupDiGetClassDevsA - ) + monkeypatch.setattr(display, "SetupDiGetClassDevsA", mockfail_SetupDiGetClassDevsA) assert display.get_edid_by_hwid(None) is None def test_device_interfaces_enum_fail(self, monkeypatch): - def mockfail_SetupDiEnumDeviceInterfaces( - hDev, devData, cGuidPtr, memberIdx, devIntData - ): + def mockfail_SetupDiEnumDeviceInterfaces(hDev, devData, cGuidPtr, memberIdx, devIntData): return False - monkeypatch.setattr( - display, "SetupDiEnumDeviceInterfaces", mockfail_SetupDiEnumDeviceInterfaces - ) + monkeypatch.setattr(display, "SetupDiEnumDeviceInterfaces", mockfail_SetupDiEnumDeviceInterfaces) assert display.get_edid_by_hwid(None) is None @@ -165,15 +157,12 @@ def fake_EnumDisplayDevicesA(device, idx, dd_ptr, flags): class TestMonitorEnumProc: - def test_happy_path(self, fake_win32, monkeypatch): monitors = DisplayInfo() monitors_ptr = py_object(monitors) lparam = addressof(monitors_ptr) - monkeypatch.setattr( - display, "find_monitor_gpu", lambda name: ("GPU-0", STATUS_OK) - ) + monkeypatch.setattr(display, "find_monitor_gpu", lambda name: ("GPU-0", STATUS_OK)) monkeypatch.setattr( display, "get_edid_by_hwid", @@ -207,9 +196,7 @@ def test_no_edid_found(self, fake_win32, monkeypatch): monitors_ptr = py_object(monitors) lparam = addressof(monitors_ptr) - monkeypatch.setattr( - display, "find_monitor_gpu", lambda name: (None, STATUS_NOK) - ) + monkeypatch.setattr(display, "find_monitor_gpu", lambda name: (None, STATUS_NOK)) monkeypatch.setattr(display, "get_edid_by_hwid", lambda hwid: None) ret = display.monitor_enum_proc(1, 0, None, lparam) @@ -235,7 +222,6 @@ def fake_EnumDisplaySettingsA(device, mode, dm_ptr): class TestDisplayInfoFetch: - def test_fetch_display_info_internal_real(self): monitors = display.fetch_display_info_internal() @@ -263,9 +249,7 @@ def test_fetch_display_info_internal_failure(self, monkeypatch): def mockfail_EnumDisplayMonitors(hdc, lprcClip, lpfnEnum, dwData): return False - monkeypatch.setattr( - display, "EnumDisplayMonitors", mockfail_EnumDisplayMonitors - ) + monkeypatch.setattr(display, "EnumDisplayMonitors", mockfail_EnumDisplayMonitors) assert display.fetch_display_info_internal().status.type == StatusType.FAILED @@ -279,9 +263,7 @@ def mockfail_EnumDisplayMonitors(hdc, lprcClip, lpfnEnum, dwData): (-1, "Unknown"), ], ) - def test_fetch_display_info_internal_orientations( - self, orientation, expected, monkeypatch - ): + def test_fetch_display_info_internal_orientations(self, orientation, expected, monkeypatch): def fake_EnumDisplaySettingsA(device, mode, dm_ptr): dm = deref(dm_ptr, display.DEVMODEA) dm.dmPelsWidth = 2560 @@ -310,10 +292,7 @@ def fake_EnumDisplayDevicesA(device, idx, dd_ptr, flags): data = display.fetch_display_info_internal() assert data.status.type == StatusType.FAILED - assert ( - data.status.messages[0] - == "Failed to fetch Display device information, PNPDeviceID is empty!" - ) + assert data.status.messages[0] == "Failed to fetch Display device information, PNPDeviceID is empty!" class TestGPU: @@ -326,22 +305,20 @@ class TestGPU: (b"", ctypes.create_string_buffer(256), 256, STATUS_INVALID_ARG), (b"\\\\.\\DISPLAY1", None, 256, STATUS_INVALID_ARG), ( - b"\\\\.\\DISPLAY1", - ctypes.create_string_buffer(256), - 0, - STATUS_INVALID_ARG, + b"\\\\.\\DISPLAY1", + ctypes.create_string_buffer(256), + 0, + STATUS_INVALID_ARG, ), ( - b"\\\\.\\DISPLAY420", - ctypes.create_string_buffer(256), - 256, - STATUS_FAILURE, + b"\\\\.\\DISPLAY420", + ctypes.create_string_buffer(256), + 256, + STATUS_FAILURE, ), ], ) - def test_fetch_display_info_gpu_display( - self, enc_name, out_buf, buf_size, exp_status, monkeypatch - ): + def test_fetch_display_info_gpu_display(self, enc_name, out_buf, buf_size, exp_status, monkeypatch): def mock_find_monitor_gpu(device_name): res = display.GetGPUForDisplay(enc_name, out_buf, buf_size) result = (None, res) @@ -368,9 +345,7 @@ def mock_find_monitor_gpu(device_name): (STATUS_FAILURE, STATUS_FAILURE), ], ) - def test_fetch_display_info_gpu_display_failures( - self, set_status, exp_status, monkeypatch - ): + def test_fetch_display_info_gpu_display_failures(self, set_status, exp_status, monkeypatch): def mockfail_GetGPUForDisplay(enc_name, out_buf, buf_size): return set_status # Simulate failure diff --git a/tests/core/windows/test_graphics.py b/tests/core/windows/test_graphics.py index af9556f..6e8cb72 100644 --- a/tests/core/windows/test_graphics.py +++ b/tests/core/windows/test_graphics.py @@ -16,16 +16,11 @@ import sys from dataclasses import dataclass from typing import Optional -from unittest.mock import patch, MagicMock - -import pytest +from unittest.mock import MagicMock, patch from hwprobe.models.status_models import StatusType -_MODULE_PATH = ( - pathlib.Path(__file__).resolve().parents[3] - / "src" / "hwprobe" / "core" / "windows" / "graphics.py" -) +_MODULE_PATH = pathlib.Path(__file__).resolve().parents[3] / "src" / "hwprobe" / "core" / "windows" / "graphics.py" def _load_graphics_module(): @@ -102,7 +97,6 @@ def _run(gpu_list): class TestHappyPath: - def test_single_gpu_success(self): info = _run([_gpu()]) @@ -130,22 +124,26 @@ def test_pcie_fields_populated(self): assert gpu.pcie_width == 16 def test_acpi_and_pci_paths_populated(self): - info = _run([_gpu( - acpi_path=r"\_SB.PCI0.PEG0.PEGP", - pci_path="PciRoot(0x0)/Pci(0x1,0x0)/Pci(0x0,0x0)", - )]) + info = _run( + [ + _gpu( + acpi_path=r"\_SB.PCI0.PEG0.PEGP", + pci_path="PciRoot(0x0)/Pci(0x1,0x0)/Pci(0x0,0x0)", + ) + ] + ) gpu = info.modules[0] assert gpu.acpi_path == r"\_SB.PCI0.PEG0.PEGP" assert gpu.pci_path == "PciRoot(0x0)/Pci(0x1,0x0)/Pci(0x0,0x0)" def test_return_type_is_graphics_info(self): from hwprobe.models.gpu_models import GraphicsInfo + info = _run([_gpu()]) assert isinstance(info, GraphicsInfo) class TestMultipleGPUs: - def test_igpu_plus_dgpu(self): igpu = _gpu( name="Intel UHD Graphics 630", @@ -180,7 +178,6 @@ def test_dual_amd_gpus(self): class TestZeroAndMissingFields: - def test_zero_vram_results_in_none(self): info = _run([_gpu(vram_mb=0)]) assert info.modules[0].vram is None @@ -203,12 +200,9 @@ def test_none_pci_path_preserved(self): class TestFailurePaths: - def test_runtime_error_returns_failed(self): mock_module = MagicMock() - mock_module.get_gpu_info.side_effect = RuntimeError( - "get_gpu_info() failed (C library returned -1)" - ) + mock_module.get_gpu_info.side_effect = RuntimeError("get_gpu_info() failed (C library returned -1)") sys.modules.pop("hwprobe.interops.win.bindings.gpu_info", None) sys.modules.pop("hwprobe.core.windows.graphics", None) @@ -230,7 +224,6 @@ def test_empty_gpu_list_returns_failed(self): class TestVendorIdFormatting: - def test_vendor_id_hex_format(self): info = _run([_gpu(vendor_id=0x10DE)]) assert info.modules[0].vendor_id == "0x10DE" diff --git a/tests/core/windows/test_network.py b/tests/core/windows/test_network.py index dffdbec..2ec5dbb 100644 --- a/tests/core/windows/test_network.py +++ b/tests/core/windows/test_network.py @@ -1,12 +1,12 @@ -import hwprobe.core.windows.network as network import pytest + +from hwprobe.core.windows import network from hwprobe.interops.win.legacy.constants import ( STATUS_FAILURE, ) -from hwprobe.models.network_models import NICInfo, NetworkInfo +from hwprobe.models.network_models import NetworkInfo, NICInfo from hwprobe.models.status_models import StatusType - # ============================================================ # Helpers # ============================================================ @@ -37,9 +37,7 @@ def mock_func(buf, size): assert network_info.status.type == StatusType.PARTIAL assert len(network_info.modules) == 2 - assert ( - network_info.modules[0].name == "Intel(R) Ethernet Connection (10) I219-V" - ) + assert network_info.modules[0].name == "Intel(R) Ethernet Connection (10) I219-V" assert network_info.modules[1].manufacturer == "Realtek" def test_empty_response_returns_failed_status(self, monkeypatch): @@ -79,9 +77,7 @@ def mock_func(buf, size): def test_bad_vendor_device_id_format(self, monkeypatch): """Test handling of bad Vendor/Device ID format in PNPDeviceID""" - mock_output = ( - "Manufacturer=Intel|PNPDeviceID=PCI\\INVALID_FORMAT|Name=Intel NIC\n" - ) + mock_output = "Manufacturer=Intel|PNPDeviceID=PCI\\INVALID_FORMAT|Name=Intel NIC\n" def mock_func(buf, size): buf.value = mock_output.encode("utf-8") @@ -95,10 +91,7 @@ def mock_func(buf, size): assert network_info.modules[0].vendor_id is None assert network_info.modules[0].device_id is None assert network_info.status.type == StatusType.PARTIAL - assert any( - "Could not parse Vendor/Device ID" in msg - for msg in network_info.status.messages - ) + assert any("Could not parse Vendor/Device ID" in msg for msg in network_info.status.messages) def test_missing_manufacturer_field(self, monkeypatch): """Test handling of missing Manufacturer field""" @@ -147,9 +140,7 @@ class TestVendorDeviceParsing: ("USB\\VID_0525&PID_A4A5", "0525", "A4A5"), ], ) - def test_parse_vendor_device_ids( - self, pnp_id, expected_vendor_id, expected_device_id, monkeypatch - ): + def test_parse_vendor_device_ids(self, pnp_id, expected_vendor_id, expected_device_id, monkeypatch): """Test parsing various vendor/device ID combinations""" mock_output = f"Manufacturer=Test|PNPDeviceID={pnp_id}|Name=Test Device\n" @@ -243,9 +234,7 @@ class TestModelStructure: def test_nic_model_fields(self, monkeypatch): """Test that NICInfo model fields are correctly populated""" - mock_output = ( - "Manufacturer=Intel|PNPDeviceID=PCI\\VEN_8086&DEV_15B8|Name=Test NIC\n" - ) + mock_output = "Manufacturer=Intel|PNPDeviceID=PCI\\VEN_8086&DEV_15B8|Name=Test NIC\n" def mock_func(buf, size): buf.value = mock_output.encode("utf-8") @@ -264,9 +253,7 @@ def mock_func(buf, size): def test_network_info_model_structure(self, monkeypatch): """Test that NetworkInfo model has correct structure""" - mock_output = ( - "Manufacturer=Intel|PNPDeviceID=PCI\\VEN_8086&DEV_15B8|Name=Test NIC\n" - ) + mock_output = "Manufacturer=Intel|PNPDeviceID=PCI\\VEN_8086&DEV_15B8|Name=Test NIC\n" def mock_func(buf, size): buf.value = mock_output.encode("utf-8") @@ -310,9 +297,7 @@ def test_various_device_counts(self, device_count, manufacturers, monkeypatch): for i, mfg in enumerate(manufacturers): vendor = f"VEN_{0x8086 + i:04X}" if i == 0 else f"VEN_{0x10EC + i:04X}" device = f"DEV_{0x15B8 + i:04X}" - lines.append( - f"Manufacturer={mfg}|PNPDeviceID=PCI\\{vendor}&{device}|Name={mfg} NIC {i + 1}" - ) + lines.append(f"Manufacturer={mfg}|PNPDeviceID=PCI\\{vendor}&{device}|Name={mfg} NIC {i + 1}") mock_output = "\n".join(lines) + "\n" def mock_func(buf, size): @@ -368,28 +353,26 @@ def mock_func(buf, size): "pci_path, acpi_path, exp_pci_path, exp_acpi_path", [ ( - "PCIROOT(0)#PCI(1D00)#PCI(0000)#PCI(0000)#PCI(0000)", - "ACPI(_SB_)#ACPI(PCI0)#ACPI(SAT0)#ACPI(NIC0)", - "PciRoot(0x0)/Pci(0x1D,0x0)/Pci(0x0,0x0)/Pci(0x0,0x0)/Pci(0x0,0x0)", - "\\_SB_.PCI0.SAT0.NIC0", + "PCIROOT(0)#PCI(1D00)#PCI(0000)#PCI(0000)#PCI(0000)", + "ACPI(_SB_)#ACPI(PCI0)#ACPI(SAT0)#ACPI(NIC0)", + "PciRoot(0x0)/Pci(0x1D,0x0)/Pci(0x0,0x0)/Pci(0x0,0x0)/Pci(0x0,0x0)", + "\\_SB_.PCI0.SAT0.NIC0", ), ( - "PCIROOT(0)#PCI(1C00)#PCI(0000)#PCI(0000)#PCI(0000)", - "ACPI(_SB_)#ACPI(PCI0)#ACPI(NIC1)", - "PciRoot(0x0)/Pci(0x1C,0x0)/Pci(0x0,0x0)/Pci(0x0,0x0)/Pci(0x0,0x0)", - "\\_SB_.PCI0.NIC1", + "PCIROOT(0)#PCI(1C00)#PCI(0000)#PCI(0000)#PCI(0000)", + "ACPI(_SB_)#ACPI(PCI0)#ACPI(NIC1)", + "PciRoot(0x0)/Pci(0x1C,0x0)/Pci(0x0,0x0)/Pci(0x0,0x0)/Pci(0x0,0x0)", + "\\_SB_.PCI0.NIC1", ), ( - "PCIROOT(0)#PCI(1400)#PCI(0000)#PCI(0000)#PCI(0000)", - "ACPI(_SB_)#ACPI(PCI0)#ACPI(SAT1)#ACPI(NIC2)", - "PciRoot(0x0)/Pci(0x14,0x0)/Pci(0x0,0x0)/Pci(0x0,0x0)/Pci(0x0,0x0)", - "\\_SB_.PCI0.SAT1.NIC2", + "PCIROOT(0)#PCI(1400)#PCI(0000)#PCI(0000)#PCI(0000)", + "ACPI(_SB_)#ACPI(PCI0)#ACPI(SAT1)#ACPI(NIC2)", + "PciRoot(0x0)/Pci(0x14,0x0)/Pci(0x0,0x0)/Pci(0x0,0x0)/Pci(0x0,0x0)", + "\\_SB_.PCI0.SAT1.NIC2", ), ], ) - def test_format_paths( - self, pci_path, acpi_path, exp_pci_path, exp_acpi_path, monkeypatch - ): + def test_format_paths(self, pci_path, acpi_path, exp_pci_path, exp_acpi_path, monkeypatch): """Test that format_{pci|acpi}_path returns correct PCI and ACPI path""" def mock_get_location_paths(pnp_device_id): From 7f097cdf094a2e04537dd1a14ed6a0a1417681ad Mon Sep 17 00:00:00 2001 From: Mahasvan Date: Sat, 1 Aug 2026 15:38:19 +0530 Subject: [PATCH 04/10] fix instruction map mismatch --- src/hwprobe/core/windows/win_enum.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/hwprobe/core/windows/win_enum.py b/src/hwprobe/core/windows/win_enum.py index 2714f9a..e00eb71 100644 --- a/src/hwprobe/core/windows/win_enum.py +++ b/src/hwprobe/core/windows/win_enum.py @@ -2,10 +2,9 @@ "SSE": 6, "SSE2": 10, "SSE3": 13, - "SSE4": 36, + "SSSE3": 36, "SSE4.1": 37, "SSE4.2": 38, - "SSE3.1": 36, } BUS_TYPE = { From 263f971b04eea34fce3c7dd08a09baf873f9a006 Mon Sep 17 00:00:00 2001 From: Mahasvan Date: Sat, 1 Aug 2026 15:38:28 +0530 Subject: [PATCH 05/10] Documentation for windows enums --- src/hwprobe/core/windows/win_enum.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/hwprobe/core/windows/win_enum.py b/src/hwprobe/core/windows/win_enum.py index e00eb71..d8bdb22 100644 --- a/src/hwprobe/core/windows/win_enum.py +++ b/src/hwprobe/core/windows/win_enum.py @@ -1,3 +1,4 @@ +# https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-isprocessorfeaturepresent FEATURE_ID_MAP = { "SSE": 6, "SSE2": 10, @@ -7,6 +8,7 @@ "SSE4.2": 38, } +# https://learn.microsoft.com/en-us/windows/win32/api/winioctl/ne-winioctl-storage_bus_type BUS_TYPE = { 0: {"type": "Unknown", "location": "Unknown"}, 1: {"type": "SCSI", "location": "Internal (Guessed)"}, @@ -29,6 +31,7 @@ 18: {"type": "Microsoft Reserved", "location": "Reserved"}, } +# https://learn.microsoft.com/en-us/windows-hardware/drivers/storage/msft-physicaldisk MEDIA_TYPE = { 0: "Unspecified", 3: "Hard Disk Drive (HDD)", @@ -36,6 +39,7 @@ 5: "Storage Class Memory (SCM)", } +# https://learn.microsoft.com/en-us/windows/win32/cimwin32prov/win32-physicalmemoryarray ECC_MEMORY_TYPE = { 0: "Reserved", 1: "Other", @@ -47,6 +51,7 @@ 7: "CRC", } +# https://learn.microsoft.com/en-us/windows/win32/cimwin32prov/win32-physicalmemory MEMORY_TYPE = { 0: "Unknown", 1: "Other", @@ -76,6 +81,7 @@ 26: "DDR4", } +# https://learn.microsoft.com/en-us/windows/win32/cimwin32prov/win32-pointingdevice POINT_DEV_INTERFACE = { 1: "Other", 2: "Unknown", From 95ced6bbc847e43c396d94b2c13e67e37fa3f87a Mon Sep 17 00:00:00 2001 From: Mahasvan Date: Sat, 1 Aug 2026 16:23:26 +0530 Subject: [PATCH 06/10] fix: Explicitly raise subprocess error in linux cpu, fix schema mismatches in mac display --- src/hwprobe/core/linux/cpu.py | 6 ++++-- src/hwprobe/core/mac/display.py | 32 ++++++++++++++++++------------- tests/core/linux/test_cpu.py | 2 +- tests/core/mac/test_display.py | 34 ++++++++++++++++----------------- 4 files changed, 41 insertions(+), 33 deletions(-) diff --git a/src/hwprobe/core/linux/cpu.py b/src/hwprobe/core/linux/cpu.py index fbed6b1..d49165e 100644 --- a/src/hwprobe/core/linux/cpu.py +++ b/src/hwprobe/core/linux/cpu.py @@ -1,3 +1,5 @@ +from __future__ import annotations + import re import subprocess from typing import Optional @@ -8,13 +10,13 @@ def _arm_cpu_cores() -> Optional[int]: try: - result = subprocess.run(["lscpu", "-p"], capture_output=True, text=True).stdout + result = subprocess.run(["lscpu", "-p"], capture_output=True, text=True, check=True).stdout lines = [x for x in result.splitlines() if not x.startswith("#")] # Format: CPU,Core,Socket,Node,,L1d,L1i,L2,L3 core_ids = [x.split(",")[1] for x in lines] # The number of distinct Core IDs is the number of cores return len(set(core_ids)) - except Exception: + except (subprocess.CalledProcessError, FileNotFoundError): return None diff --git a/src/hwprobe/core/mac/display.py b/src/hwprobe/core/mac/display.py index caffb23..f2e67ce 100644 --- a/src/hwprobe/core/mac/display.py +++ b/src/hwprobe/core/mac/display.py @@ -1,10 +1,12 @@ import json import re import subprocess +from subprocess import CalledProcessError from typing import Optional from hwprobe.core.common.edid import parse_edid from hwprobe.models.display_models import DisplayInfo, DisplayModuleInfo, ResolutionInfo +from hwprobe.models.status_models import Status def _get_monitor_resolution_from_system_profiler(monitor_info: dict) -> Optional[tuple[int, int]]: @@ -60,36 +62,38 @@ def _get_refresh_rate_from_system_profiler(monitor_info: dict) -> Optional[float return None -def _fetch_monitor_info_system_profiler(): +def _fetch_monitor_info_system_profiler() -> tuple[list[DisplayModuleInfo], Status]: monitors = [] command = ["system_profiler", "-json", "SPDisplaysDataType"] try: - output = json.loads(subprocess.run(command, capture_output=True, text=True).stdout) - except (json.JSONDecodeError, FileNotFoundError): + output = json.loads(subprocess.run(command, capture_output=True, text=True, check=True).stdout) + except (json.JSONDecodeError, FileNotFoundError, CalledProcessError): output = {} # # todo: remove after testing # with open("quangle.json") as f: # output = json.load(f) + display_status = Status() + for display_controller in output.get("SPDisplaysDataType", []): monitor_instances = display_controller.get("spdisplays_ndrvs", []) - for monitor in monitor_instances: + for i, monitor in enumerate(monitor_instances): monitor_info = DisplayModuleInfo() if name := monitor.get("_name"): monitor_info.name = name else: - monitor_info.status.make_partial("Could not retrieve monitor name from system profiler") + display_status.make_partial(f"Could not retrieve name from system profiler for monitor {i}") if serial := monitor.get("_spdisplays_display-serial-number"): monitor_info.serial_number = serial else: - monitor_info.status.make_partial("Could not retrieve serial number from system profiler") + display_status.make_partial(f"Could not retrieve serial number from system profiler for monitor {i}") if year := monitor.get("_spdisplays_display-year"): monitor_info.year = int(year) else: - monitor_info.status.make_partial("Could not retrieve year from system profiler") + display_status.make_partial(f"Could not retrieve year from system profiler for monitor {i}") retrieved_resolution = _get_monitor_resolution_from_system_profiler(monitor) retrieved_refresh_rate = _get_refresh_rate_from_system_profiler(monitor) @@ -97,16 +101,16 @@ def _fetch_monitor_info_system_profiler(): if retrieved_resolution: res.width, res.height = retrieved_resolution else: - monitor_info.status.make_partial("Could not retrieve resolution from system profiler") + display_status.make_partial(f"Could not retrieve resolution from system profiler for monitor {i}") if retrieved_refresh_rate: res.refresh_rate = retrieved_refresh_rate else: - monitor_info.status.make_partial("Could not retrieve refresh rate from system profiler") + display_status.make_partial(f"Could not retrieve refresh rate from system profiler for monitor {i}") monitor_info.resolution = res monitor_info.gpu_name = display_controller.get("sppci_model") if not monitor_info.gpu_name: - monitor_info.status.make_partial("Could not retrieve GPU name from system profiler") + display_status.make_partial(f"Could not retrieve GPU name from system profiler for monitor {i}") # Backup name just in case, sometimes is the same, # sometimes is something like `kHW_AMDRadeonPro560XItem` monitor_info.gpu_name = display_controller.get("_name") @@ -114,14 +118,16 @@ def _fetch_monitor_info_system_profiler(): if edid := monitor.get("_spdisplays_edid"): monitor_info = _enrich_data_from_edid(monitor_info, edid) else: - monitor_info.status.make_partial("Could not retrieve EDID from system profiler. Is this Apple Silicon?") + display_status.make_partial( + f"Could not retrieve EDID from system profiler for monitor {i}. Is this Apple Silicon?" + ) monitors.append(monitor_info) - return monitors + return monitors, display_status def fetch_display_info() -> DisplayInfo: display_info = DisplayInfo() - display_info.modules = _fetch_monitor_info_system_profiler() + display_info.modules, display_info.status = _fetch_monitor_info_system_profiler() return display_info diff --git a/tests/core/linux/test_cpu.py b/tests/core/linux/test_cpu.py index c21f104..21093c7 100644 --- a/tests/core/linux/test_cpu.py +++ b/tests/core/linux/test_cpu.py @@ -43,7 +43,7 @@ def mock_run(*args, **kwargs): def test_arm_cpu_cores_failure(self, monkeypatch): def mock_run(*args, **kwargs): - raise RuntimeError("lscpu failed") + raise subprocess.CalledProcessError(1, "lscpu") monkeypatch.setattr(subprocess, "run", mock_run) diff --git a/tests/core/mac/test_display.py b/tests/core/mac/test_display.py index 8e0f100..514cfb0 100644 --- a/tests/core/mac/test_display.py +++ b/tests/core/mac/test_display.py @@ -184,7 +184,7 @@ def test_single_monitor_basic_info(self, mock_run): mock_result.stdout = json.dumps(sp_data) mock_run.return_value = mock_result - monitors = _fetch_monitor_info_system_profiler() + monitors, status = _fetch_monitor_info_system_profiler() assert len(monitors) == 1 m = monitors[0] @@ -211,10 +211,10 @@ def test_missing_name_is_partial(self, mock_run): mock_result.stdout = json.dumps(sp_data) mock_run.return_value = mock_result - monitors = _fetch_monitor_info_system_profiler() + monitors, status = _fetch_monitor_info_system_profiler() assert len(monitors) == 1 - assert monitors[0].status.type == StatusType.PARTIAL - assert any("name" in m.lower() for m in monitors[0].status.messages) + assert status.type == StatusType.PARTIAL + assert any("name" in m.lower() for m in status.messages) @patch("hwprobe.core.mac.display.subprocess.run") def test_missing_serial_is_partial(self, mock_run): @@ -232,9 +232,9 @@ def test_missing_serial_is_partial(self, mock_run): mock_result.stdout = json.dumps(sp_data) mock_run.return_value = mock_result - monitors = _fetch_monitor_info_system_profiler() - assert monitors[0].status.type == StatusType.PARTIAL - assert any("serial" in m.lower() for m in monitors[0].status.messages) + monitors, status = _fetch_monitor_info_system_profiler() + assert status.type == StatusType.PARTIAL + assert any("serial" in m.lower() for m in status.messages) @patch("hwprobe.core.mac.display.subprocess.run") def test_missing_year_is_partial(self, mock_run): @@ -253,8 +253,8 @@ def test_missing_year_is_partial(self, mock_run): mock_result.stdout = json.dumps(sp_data) mock_run.return_value = mock_result - monitors = _fetch_monitor_info_system_profiler() - assert any("year" in m.lower() for m in monitors[0].status.messages) + monitors, status = _fetch_monitor_info_system_profiler() + assert any("year" in m.lower() for m in status.messages) @patch("hwprobe.core.mac.display.subprocess.run") def test_missing_gpu_name_is_partial(self, mock_run): @@ -276,8 +276,8 @@ def test_missing_gpu_name_is_partial(self, mock_run): mock_result.stdout = json.dumps(sp_data) mock_run.return_value = mock_result - monitors = _fetch_monitor_info_system_profiler() - assert any("GPU" in m for m in monitors[0].status.messages) + monitors, status = _fetch_monitor_info_system_profiler() + assert any("GPU" in m for m in status.messages) @patch("hwprobe.core.mac.display.subprocess.run") def test_empty_sp_output_returns_empty_list(self, mock_run): @@ -285,7 +285,7 @@ def test_empty_sp_output_returns_empty_list(self, mock_run): mock_result.stdout = json.dumps({}) mock_run.return_value = mock_result - monitors = _fetch_monitor_info_system_profiler() + monitors, status = _fetch_monitor_info_system_profiler() assert monitors == [] @patch("hwprobe.core.mac.display.subprocess.run") @@ -294,7 +294,7 @@ def test_json_decode_error_returns_empty_list(self, mock_run): mock_result.stdout = "NOT JSON" mock_run.return_value = mock_result - monitors = _fetch_monitor_info_system_profiler() + monitors, status = _fetch_monitor_info_system_profiler() assert monitors == [] @patch("hwprobe.core.mac.display.subprocess.run") @@ -309,7 +309,7 @@ def test_multiple_monitors_across_controllers(self, mock_run): mock_result.stdout = json.dumps(sp_data) mock_run.return_value = mock_result - monitors = _fetch_monitor_info_system_profiler() + monitors, status = _fetch_monitor_info_system_profiler() assert len(monitors) == 2 assert monitors[0].name == "Monitor A" assert monitors[1].name == "Monitor B" @@ -337,7 +337,7 @@ def test_edid_enrichment_called_when_present(self, mock_run): mock_result.stdout = json.dumps(sp_data) mock_run.return_value = mock_result - monitors = _fetch_monitor_info_system_profiler() + monitors, status = _fetch_monitor_info_system_profiler() assert len(monitors) == 1 @patch("hwprobe.core.mac.display.subprocess.run") @@ -359,8 +359,8 @@ def test_missing_edid_is_partial(self, mock_run): mock_result.stdout = json.dumps(sp_data) mock_run.return_value = mock_result - monitors = _fetch_monitor_info_system_profiler() - assert any("EDID" in m for m in monitors[0].status.messages) + monitors, status = _fetch_monitor_info_system_profiler() + assert any("EDID" in m for m in status.messages) # ── fetch_display_info ─────────────────────────────────────────────────────── From 899eb0f3e3b33fb6229c15ca88dd77f3c25fbd15 Mon Sep 17 00:00:00 2001 From: Mahasvan Date: Sat, 1 Aug 2026 17:32:24 +0530 Subject: [PATCH 07/10] patch: Add checks for command failures in mac network --- src/hwprobe/core/mac/network.py | 52 +++++++++++++++++++++++---------- 1 file changed, 36 insertions(+), 16 deletions(-) diff --git a/src/hwprobe/core/mac/network.py b/src/hwprobe/core/mac/network.py index bf44948..04327da 100644 --- a/src/hwprobe/core/mac/network.py +++ b/src/hwprobe/core/mac/network.py @@ -4,16 +4,26 @@ from typing import Optional from hwprobe.models.network_models import NetworkInfo, NICInfo +from hwprobe.models.status_models import StatusType + +_IO_NAME_PATTERN = re.compile(r"pci([0-9a-fA-F]{4}),([0-9a-fA-F]{4})") def _fetch_controllers() -> list[str]: - output = subprocess.run(["ipconfig", "getiflist"], capture_output=True) + try: + output = subprocess.run(["ipconfig", "getiflist"], capture_output=True, check=True) + except subprocess.CalledProcessError: + return [] stripped = output.stdout.decode("utf-8").strip() return stripped.split(" ") if stripped else [] def _fetch_ethernet_details() -> dict[str, NICInfo]: - output = subprocess.run(["system_profiler", "SPEthernetDataType", "-xml"], capture_output=True) + try: + output = subprocess.run(["system_profiler", "SPEthernetDataType", "-xml"], capture_output=True, check=True) + except subprocess.CalledProcessError: + return {} + plist = plistlib.loads(output.stdout) res = {} for item in plist: @@ -97,13 +107,15 @@ def _fetch_airport_details() -> dict[str, NICInfo]: Earlier, `system_profiler SPAirPortDataType -xml` was used to get the vendor and device id. However, this was too slow, and we can get the same details from `ioreg`, while it being faster. """ - output = subprocess.run(["ioreg", "-c", "IO80211Controller", "-r", "-a"], capture_output=True) + try: + output = subprocess.run(["ioreg", "-c", "IO80211Controller", "-r", "-a"], capture_output=True, check=True) + except subprocess.CalledProcessError: + return {} + plist = plistlib.loads(output.stdout) res = {} - io_name_pattern = re.compile(r"pci([0-9a-fA-F]{4}),([0-9a-fA-F]{4})") - for item in plist: driver = item.get("IORegistryEntryName") @@ -114,7 +126,9 @@ def _fetch_airport_details() -> dict[str, NICInfo]: # Intel Macs, usually io_name = item.get("IONameMatched", "") io_model = item.get("IOModel", "") - match = io_name_pattern.match(io_name) + match = _IO_NAME_PATTERN.match(io_name) + if not match: + continue vendor, device = match.groups() nic_info = NICInfo() @@ -123,12 +137,9 @@ def _fetch_airport_details() -> dict[str, NICInfo]: if io_model: nic_info.name = io_model - for child in item.get("IORegistryEntryChildren", []): - if not child.get("IOObjectClass", "") == "AirPort_BrcmNIC_Interface": - continue - bcm_identifier = child.get("IORegistryEntryName") - res[bcm_identifier] = nic_info - break + child = _find_child(item.get("IORegistryEntryChildren", []), "IOObjectClass", "AirPort_BrcmNIC_Interface") + if child: + res[child.get("IORegistryEntryName")] = nic_info elif driver == "AppleBCMWLANCore": # Most Apple Silicon Macs @@ -147,12 +158,14 @@ def _fetch_airport_details() -> dict[str, NICInfo]: elif driver == "AppleWLANDriver": # Wi-Fi 7 driver for the M5 series - device_info = item.get("AirshipDeviceCriteria") + device_info = item.get("AirshipDeviceCriteria") or {} chipset = device_info.get("Chipset") vendor = device_info.get("Vendor") bsd_identifier = _get_bsd_interface_apple_silicon(item, driver=driver) + if not bsd_identifier: + continue nic_info = NICInfo() if vendor: @@ -169,7 +182,7 @@ def _fetch_airport_details() -> dict[str, NICInfo]: # Older Intel Macs with Broadcom BCM4331 chipset io_name = item.get("IONameMatched", "") io_model = item.get("IOModel", "") - match = io_name_pattern.match(io_name) + match = _IO_NAME_PATTERN.match(io_name) if match: vendor, device = match.groups() @@ -196,10 +209,17 @@ def _fetch_airport_details() -> dict[str, NICInfo]: def _fetch_system_profiler_details(valid_bsd_interfaces: list[str]) -> NetworkInfo: - output = subprocess.run(["system_profiler", "SPNetworkDataType", "-xml"], capture_output=True) - plist = plistlib.loads(output.stdout) network_info = NetworkInfo() + try: + output = subprocess.run(["system_profiler", "SPNetworkDataType", "-xml"], capture_output=True, check=True) + except subprocess.CalledProcessError as e: + network_info.status.type = StatusType.FAILED + network_info.status.messages.append(f"Could not get system_profiler output: {e}") + return network_info + + plist = plistlib.loads(output.stdout) + ethernet_info: Optional[dict[str, NICInfo]] = None airport_info: Optional[dict[str, NICInfo]] = None From 7fc4f2307fd21aa25eefeda6165f1f2531441cc5 Mon Sep 17 00:00:00 2001 From: Mahasvan Date: Sat, 1 Aug 2026 17:35:49 +0530 Subject: [PATCH 08/10] fix/test: empty NICs added to linux network, add linux network tests --- src/hwprobe/core/linux/network.py | 2 + tests/core/linux/test_network.py | 442 ++++++++++++++++++++++++++++++ 2 files changed, 444 insertions(+) create mode 100644 tests/core/linux/test_network.py diff --git a/src/hwprobe/core/linux/network.py b/src/hwprobe/core/linux/network.py index 8c870ce..1638a05 100644 --- a/src/hwprobe/core/linux/network.py +++ b/src/hwprobe/core/linux/network.py @@ -59,6 +59,8 @@ def _fetch_ip_data() -> NetworkInfo: for row in data: nic = NICInfo() ifname = row.get("ifname") + if not ifname: + continue nic.interface = ifname nic.type = row.get("link_type") diff --git a/tests/core/linux/test_network.py b/tests/core/linux/test_network.py new file mode 100644 index 0000000..c5394a5 --- /dev/null +++ b/tests/core/linux/test_network.py @@ -0,0 +1,442 @@ +import builtins +import json +import os +import subprocess +from unittest.mock import mock_open + +import pytest + +from hwprobe.core.linux.network import _enrich_with_sysfs_info, _fetch_ip_data, fetch_network_info +from hwprobe.models.network_models import NICInfo, NetworkInfo +from hwprobe.models.status_models import Status, StatusType + + +class TestEnrichWithSysfsInfo: + DEVICE_PATH = "/sys/class/net/eth0/device" + + def _make_nic(self, interface="eth0"): + return NICInfo(interface=interface) + + def _patch_exists(self, monkeypatch, paths): + monkeypatch.setattr(os.path, "exists", lambda p: p in paths) + + def test_no_interface_returns_early(self): + nic = NICInfo(interface=None) + status = Status() + _enrich_with_sysfs_info(nic, status) + assert nic.vendor_id is None + assert status.type == StatusType.SUCCESS + + def test_virtual_interface_raises_value_error(self, monkeypatch): + nic = self._make_nic() + self._patch_exists(monkeypatch, set()) + status = Status() + with pytest.raises(ValueError, match="Interface is virtual: eth0"): + _enrich_with_sysfs_info(nic, status) + + def test_vendor_and_device_read(self, monkeypatch): + nic = self._make_nic() + self._patch_exists(monkeypatch, {self.DEVICE_PATH}) + + def fake_open(path, *args, **kwargs): + if path.endswith("/vendor"): + return mock_open(read_data="0x8086")() + if path.endswith("/device"): + return mock_open(read_data="0x1572")() + if path.endswith("/firmware_node/path"): + return mock_open(read_data=r"\_SB.PCI0.GLAN")() + raise FileNotFoundError(path) + + monkeypatch.setattr(builtins, "open", fake_open) + monkeypatch.setattr(os.path, "realpath", lambda p: "/sys/devices/pci0000:00/0000:00:1f.6") + monkeypatch.setattr("hwprobe.core.linux.network.pci_path_linux", lambda s: "PciRoot(0x0)/Pci(0x1f,0x6)") + + status = Status() + _enrich_with_sysfs_info(nic, status) + + assert nic.vendor_id == "0x8086" + assert nic.device_id == "0x1572" + assert nic.pci_path == "PciRoot(0x0)/Pci(0x1f,0x6)" + assert status.type == StatusType.SUCCESS + + def test_missing_vendor_makes_partial(self, monkeypatch): + nic = self._make_nic() + self._patch_exists(monkeypatch, {self.DEVICE_PATH}) + + def fake_open(path, *args, **kwargs): + if path.endswith("/vendor"): + raise FileNotFoundError(path) + if path.endswith("/device"): + return mock_open(read_data="0x1572")() + raise FileNotFoundError(path) + + monkeypatch.setattr(builtins, "open", fake_open) + monkeypatch.setattr(os.path, "realpath", lambda p: "/sys/devices/pci0000:00/0000:00:1f.6") + monkeypatch.setattr("hwprobe.core.linux.network.pci_path_linux", lambda s: None) + + status = Status() + _enrich_with_sysfs_info(nic, status) + + assert nic.vendor_id is None + assert nic.device_id == "0x1572" + assert status.type == StatusType.PARTIAL + assert any("Vendor ID not found" in m for m in status.messages) + + def test_missing_device_makes_partial(self, monkeypatch): + nic = self._make_nic() + self._patch_exists(monkeypatch, {self.DEVICE_PATH}) + + def fake_open(path, *args, **kwargs): + if path.endswith("/vendor"): + return mock_open(read_data="0x8086")() + if path.endswith("/device"): + raise FileNotFoundError(path) + raise FileNotFoundError(path) + + monkeypatch.setattr(builtins, "open", fake_open) + monkeypatch.setattr(os.path, "realpath", lambda p: "/sys/devices/pci0000:00/0000:00:1f.6") + monkeypatch.setattr("hwprobe.core.linux.network.pci_path_linux", lambda s: None) + + status = Status() + _enrich_with_sysfs_info(nic, status) + + assert nic.vendor_id == "0x8086" + assert nic.device_id is None + assert status.type == StatusType.PARTIAL + assert any("Device ID not found" in m for m in status.messages) + + def test_acpi_path_populated(self, monkeypatch): + nic = self._make_nic() + self._patch_exists(monkeypatch, {self.DEVICE_PATH}) + + def fake_open(path, *args, **kwargs): + if path.endswith("/vendor"): + return mock_open(read_data="0x8086")() + if path.endswith("/device"): + return mock_open(read_data="0x1572")() + if path.endswith("/firmware_node/path"): + return mock_open(read_data=r"\_SB.PCI0.GLAN")() + raise FileNotFoundError(path) + + monkeypatch.setattr(builtins, "open", fake_open) + monkeypatch.setattr(os.path, "realpath", lambda p: "/sys/devices/pci0000:00/0000:00:1f.6") + monkeypatch.setattr("hwprobe.core.linux.network.pci_path_linux", lambda s: None) + + status = Status() + _enrich_with_sysfs_info(nic, status) + + assert nic.acpi_path == r"\_SB.PCI0.GLAN" + + def test_missing_acpi_path_makes_partial(self, monkeypatch): + nic = self._make_nic() + self._patch_exists(monkeypatch, {self.DEVICE_PATH}) + + def fake_open(path, *args, **kwargs): + if path.endswith("/vendor"): + return mock_open(read_data="0x8086")() + if path.endswith("/device"): + return mock_open(read_data="0x1572")() + raise FileNotFoundError(path) + + monkeypatch.setattr(builtins, "open", fake_open) + monkeypatch.setattr(os.path, "realpath", lambda p: "/sys/devices/pci0000:00/0000:00:1f.6") + monkeypatch.setattr("hwprobe.core.linux.network.pci_path_linux", lambda s: None) + + status = Status() + _enrich_with_sysfs_info(nic, status) + + assert status.type == StatusType.PARTIAL + assert any("Path not found" in m for m in status.messages) + + def test_pci_path_resolved_from_realpath(self, monkeypatch): + nic = self._make_nic() + self._patch_exists(monkeypatch, {self.DEVICE_PATH}) + + def fake_open(path, *args, **kwargs): + if path.endswith("/vendor"): + return mock_open(read_data="0x8086")() + if path.endswith("/device"): + return mock_open(read_data="0x1572")() + raise FileNotFoundError(path) + + monkeypatch.setattr(builtins, "open", fake_open) + monkeypatch.setattr(os.path, "realpath", lambda p: "/sys/devices/pci0000:00/0000:01:00.0") + + pci_calls = [] + monkeypatch.setattr( + "hwprobe.core.linux.network.pci_path_linux", + lambda s: (pci_calls.append(s), "PciRoot(0x0)/Pci(0x1,0x0)")[-1], + ) + + status = Status() + _enrich_with_sysfs_info(nic, status) + + assert pci_calls == ["0000:01:00.0"] + assert nic.pci_path == "PciRoot(0x0)/Pci(0x1,0x0)" + + +class TestFetchIpData: + def _mock_ip_output(self, rows): + return json.dumps(rows).encode() + + def test_single_ethernet_interface(self, monkeypatch): + rows = [ + { + "ifname": "eth0", + "link_type": "ether", + "address": "aa:bb:cc:dd:ee:ff", + "addr_info": [{"family": "inet", "local": "192.168.1.10"}], + } + ] + monkeypatch.setattr(subprocess, "check_output", lambda *a, **kw: self._mock_ip_output(rows)) + monkeypatch.setattr(os.path, "exists", lambda p: False) # virtual -> skipped enrichment + + info = _fetch_ip_data() + assert len(info.modules) == 0 # virtual interface, skipped + + def test_physical_interface_enriched(self, monkeypatch): + rows = [ + { + "ifname": "eth0", + "link_type": "ether", + "address": "aa:bb:cc:dd:ee:ff", + "addr_info": [{"family": "inet", "local": "10.0.0.5"}], + } + ] + monkeypatch.setattr(subprocess, "check_output", lambda *a, **kw: self._mock_ip_output(rows)) + monkeypatch.setattr(os.path, "exists", lambda p: p == "/sys/class/net/eth0/device") + + def fake_open(path, *args, **kwargs): + if path.endswith("/vendor"): + return mock_open(read_data="0x8086")() + if path.endswith("/device"): + return mock_open(read_data="0x1572")() + raise FileNotFoundError(path) + + monkeypatch.setattr(builtins, "open", fake_open) + monkeypatch.setattr(os.path, "realpath", lambda p: "/sys/devices/pci0000:00/0000:00:1f.6") + monkeypatch.setattr("hwprobe.core.linux.network.pci_path_linux", lambda s: None) + + info = _fetch_ip_data() + assert len(info.modules) == 1 + nic = info.modules[0] + assert nic.interface == "eth0" + assert nic.type == "ether" + assert nic.mac_address == "aa:bb:cc:dd:ee:ff" + assert nic.ip_address == "10.0.0.5" + assert nic.vendor_id == "0x8086" + assert nic.device_id == "0x1572" + + def test_ipv4_preferred_over_ipv6(self, monkeypatch): + rows = [ + { + "ifname": "eth0", + "link_type": "ether", + "address": "aa:bb:cc:dd:ee:ff", + "addr_info": [ + {"family": "inet6", "local": "fe80::1"}, + {"family": "inet", "local": "172.16.0.2"}, + ], + } + ] + monkeypatch.setattr(subprocess, "check_output", lambda *a, **kw: self._mock_ip_output(rows)) + monkeypatch.setattr(os.path, "exists", lambda p: p == "/sys/class/net/eth0/device") + + def fake_open(path, *args, **kwargs): + if path.endswith("/vendor"): + return mock_open(read_data="0x10ec")() + if path.endswith("/device"): + return mock_open(read_data="0x8168")() + raise FileNotFoundError(path) + + monkeypatch.setattr(builtins, "open", fake_open) + monkeypatch.setattr(os.path, "realpath", lambda p: "/sys/devices/pci0000:00/0000:00:1f.6") + monkeypatch.setattr("hwprobe.core.linux.network.pci_path_linux", lambda s: None) + + info = _fetch_ip_data() + assert info.modules[0].ip_address == "172.16.0.2" + + def test_ipv6_fallback_when_no_ipv4(self, monkeypatch): + rows = [ + { + "ifname": "eth0", + "link_type": "ether", + "address": "aa:bb:cc:dd:ee:ff", + "addr_info": [{"family": "inet6", "local": "fe80::abcd"}], + } + ] + monkeypatch.setattr(subprocess, "check_output", lambda *a, **kw: self._mock_ip_output(rows)) + monkeypatch.setattr(os.path, "exists", lambda p: p == "/sys/class/net/eth0/device") + + def fake_open(path, *args, **kwargs): + if path.endswith("/vendor"): + return mock_open(read_data="0x10ec")() + if path.endswith("/device"): + return mock_open(read_data="0x8168")() + raise FileNotFoundError(path) + + monkeypatch.setattr(builtins, "open", fake_open) + monkeypatch.setattr(os.path, "realpath", lambda p: "/sys/devices/pci0000:00/0000:00:1f.6") + monkeypatch.setattr("hwprobe.core.linux.network.pci_path_linux", lambda s: None) + + info = _fetch_ip_data() + assert info.modules[0].ip_address == "fe80::abcd" + + def test_no_ip_address(self, monkeypatch): + rows = [ + { + "ifname": "eth0", + "link_type": "ether", + "address": "aa:bb:cc:dd:ee:ff", + "addr_info": [], + } + ] + monkeypatch.setattr(subprocess, "check_output", lambda *a, **kw: self._mock_ip_output(rows)) + monkeypatch.setattr(os.path, "exists", lambda p: p == "/sys/class/net/eth0/device") + + def fake_open(path, *args, **kwargs): + if path.endswith("/vendor"): + return mock_open(read_data="0x10ec")() + if path.endswith("/device"): + return mock_open(read_data="0x8168")() + raise FileNotFoundError(path) + + monkeypatch.setattr(builtins, "open", fake_open) + monkeypatch.setattr(os.path, "realpath", lambda p: "/sys/devices/pci0000:00/0000:00:1f.6") + monkeypatch.setattr("hwprobe.core.linux.network.pci_path_linux", lambda s: None) + + info = _fetch_ip_data() + assert info.modules[0].ip_address is None + + def test_multiple_interfaces(self, monkeypatch): + rows = [ + { + "ifname": "eth0", + "link_type": "ether", + "address": "aa:bb:cc:dd:ee:ff", + "addr_info": [{"family": "inet", "local": "10.0.0.1"}], + }, + { + "ifname": "wlan0", + "link_type": "ether", + "address": "11:22:33:44:55:66", + "addr_info": [{"family": "inet", "local": "10.0.0.2"}], + }, + ] + monkeypatch.setattr(subprocess, "check_output", lambda *a, **kw: self._mock_ip_output(rows)) + monkeypatch.setattr(os.path, "exists", lambda p: p in { + "/sys/class/net/eth0/device", + "/sys/class/net/wlan0/device", + }) + + def fake_open(path, *args, **kwargs): + if path.endswith("/vendor"): + return mock_open(read_data="0x8086")() + if path.endswith("/device"): + return mock_open(read_data="0x1572")() + raise FileNotFoundError(path) + + monkeypatch.setattr(builtins, "open", fake_open) + monkeypatch.setattr(os.path, "realpath", lambda p: "/sys/devices/pci0000:00/0000:00:1f.6") + monkeypatch.setattr("hwprobe.core.linux.network.pci_path_linux", lambda s: None) + + info = _fetch_ip_data() + assert len(info.modules) == 2 + assert {m.interface for m in info.modules} == {"eth0", "wlan0"} + + def test_virtual_interfaces_skipped(self, monkeypatch): + rows = [ + {"ifname": "lo", "link_type": "loopback", "address": "00:00:00:00:00:00", "addr_info": []}, + {"ifname": "docker0", "link_type": "ether", "address": "02:42:00:00:00:00", "addr_info": []}, + ] + monkeypatch.setattr(subprocess, "check_output", lambda *a, **kw: self._mock_ip_output(rows)) + monkeypatch.setattr(os.path, "exists", lambda p: False) + + info = _fetch_ip_data() + assert len(info.modules) == 0 + + def test_mixed_virtual_and_physical(self, monkeypatch): + rows = [ + {"ifname": "lo", "link_type": "loopback", "address": "00:00:00:00:00:00", "addr_info": []}, + { + "ifname": "eth0", + "link_type": "ether", + "address": "aa:bb:cc:dd:ee:ff", + "addr_info": [{"family": "inet", "local": "10.0.0.1"}], + }, + ] + monkeypatch.setattr(subprocess, "check_output", lambda *a, **kw: self._mock_ip_output(rows)) + monkeypatch.setattr(os.path, "exists", lambda p: p == "/sys/class/net/eth0/device") + + def fake_open(path, *args, **kwargs): + if path.endswith("/vendor"): + return mock_open(read_data="0x8086")() + if path.endswith("/device"): + return mock_open(read_data="0x1572")() + raise FileNotFoundError(path) + + monkeypatch.setattr(builtins, "open", fake_open) + monkeypatch.setattr(os.path, "realpath", lambda p: "/sys/devices/pci0000:00/0000:00:1f.6") + monkeypatch.setattr("hwprobe.core.linux.network.pci_path_linux", lambda s: None) + + info = _fetch_ip_data() + assert len(info.modules) == 1 + assert info.modules[0].interface == "eth0" + + def test_missing_ifname_skipped(self, monkeypatch): + rows = [ + {"link_type": "ether", "address": "aa:bb:cc:dd:ee:ff", "addr_info": []}, + ] + monkeypatch.setattr(subprocess, "check_output", lambda *a, **kw: self._mock_ip_output(rows)) + monkeypatch.setattr(os.path, "exists", lambda p: False) + + info = _fetch_ip_data() + assert len(info.modules) == 0 + + def test_empty_addr_info_list(self, monkeypatch): + rows = [ + { + "ifname": "eth0", + "link_type": "ether", + "address": "aa:bb:cc:dd:ee:ff", + "addr_info": [], + } + ] + monkeypatch.setattr(subprocess, "check_output", lambda *a, **kw: self._mock_ip_output(rows)) + monkeypatch.setattr(os.path, "exists", lambda p: p == "/sys/class/net/eth0/device") + + def fake_open(path, *args, **kwargs): + if path.endswith("/vendor"): + return mock_open(read_data="0x8086")() + if path.endswith("/device"): + return mock_open(read_data="0x1572")() + raise FileNotFoundError(path) + + monkeypatch.setattr(builtins, "open", fake_open) + monkeypatch.setattr(os.path, "realpath", lambda p: "/sys/devices/pci0000:00/0000:00:1f.6") + monkeypatch.setattr("hwprobe.core.linux.network.pci_path_linux", lambda s: None) + + info = _fetch_ip_data() + assert info.modules[0].ip_address is None + + +class TestFetchNetworkInfo: + def test_returns_network_info_type(self, monkeypatch): + monkeypatch.setattr( + "hwprobe.core.linux.network._fetch_ip_data", + lambda: NetworkInfo(), + ) + info = fetch_network_info() + assert isinstance(info, NetworkInfo) + assert info.modules == [] + + def test_propagates_modules(self, monkeypatch): + nic = NICInfo(interface="eth0", mac_address="aa:bb:cc:dd:ee:ff") + expected = NetworkInfo(modules=[nic]) + monkeypatch.setattr( + "hwprobe.core.linux.network._fetch_ip_data", + lambda: expected, + ) + info = fetch_network_info() + assert len(info.modules) == 1 + assert info.modules[0].interface == "eth0" From a3c83cdaf19cef0f81191f31bef5931c24d64975 Mon Sep 17 00:00:00 2001 From: Mahasvan Date: Sat, 1 Aug 2026 17:41:52 +0530 Subject: [PATCH 09/10] patch: code quality improvements, bug fixes --- src/hwprobe/core/linux/cpu.py | 2 +- src/hwprobe/core/linux/graphics.py | 9 +++------ src/hwprobe/core/linux/storage.py | 30 ++++++++++++++++++------------ src/hwprobe/util/location_paths.py | 2 +- 4 files changed, 23 insertions(+), 20 deletions(-) diff --git a/src/hwprobe/core/linux/cpu.py b/src/hwprobe/core/linux/cpu.py index d49165e..08f48bc 100644 --- a/src/hwprobe/core/linux/cpu.py +++ b/src/hwprobe/core/linux/cpu.py @@ -168,7 +168,7 @@ def fetch_cpu_info() -> CPUInfo: cpu_info.status.messages.append("/proc/cpuinfo has no content") return cpu_info - architecture = subprocess.run(["uname", "-m"], capture_output=True, text=True) + architecture = subprocess.run(["uname", "-m"], capture_output=True, text=True, check=True) if ("aarch64" in architecture.stdout) or ("arm" in architecture.stdout): return fetch_arm_cpu_info(raw_cpu_info) diff --git a/src/hwprobe/core/linux/graphics.py b/src/hwprobe/core/linux/graphics.py index 8348364..c12c483 100644 --- a/src/hwprobe/core/linux/graphics.py +++ b/src/hwprobe/core/linux/graphics.py @@ -93,12 +93,9 @@ def _populate_nvidia_info(gpu: GPUInfo, device: str) -> GPUInfo: def _populate_lspci_info(gpu: GPUInfo, device: str) -> GPUInfo: - try: - lspci_output = subprocess.run(["lspci", "-s", device, "-vmm"], capture_output=True, text=True).stdout - # We gather all data here and parse whatever data we have. Subsystem data may not be returned. - except Exception: - # lspci may not be available in some distros - raise + lspci_output = subprocess.run(["lspci", "-s", device, "-vmm"], capture_output=True, text=True, check=True).stdout + # We gather all data here and parse whatever data we have. Subsystem data may not be returned. + # If LSPCI not found, check=True ensures error is thrown data = {} for line in lspci_output.splitlines(): diff --git a/src/hwprobe/core/linux/storage.py b/src/hwprobe/core/linux/storage.py index 1f11090..a38d1a7 100644 --- a/src/hwprobe/core/linux/storage.py +++ b/src/hwprobe/core/linux/storage.py @@ -5,6 +5,12 @@ from hwprobe.models.storage_models import DiskInfo, StorageInfo +def _read_sysfs(path: str) -> str: + """Read a sysfs file and return its stripped contents.""" + with open(path) as f: + return f.read().strip() + + def _fetch_emmc_info(folder: str) -> tuple[DiskInfo, Status]: """ Helper function for eMMC devices, which have different places to get some data. @@ -18,13 +24,13 @@ def _fetch_emmc_info(folder: str) -> tuple[DiskInfo, Status]: disk.identifier = folder.strip() - model = open(f"{path}/device/name").read().strip() + model = _read_sysfs(f"{path}/device/name") disk.model = model if not model: status.type = StatusType.PARTIAL status.messages.append("Disk Model could not be found") - removable = open(f"{path}/removable").read().strip() + removable = _read_sysfs(f"{path}/removable") if removable == "0": disk.type = "Embedded MultiMediaCard (eMMC)" @@ -34,19 +40,19 @@ def _fetch_emmc_info(folder: str) -> tuple[DiskInfo, Status]: disk.location = "Internal" if removable == "0" else "External" disk.connector = "Unknown" - vendor_id = open(f"{path}/device/manfid").read().strip() + vendor_id = _read_sysfs(f"{path}/device/manfid") disk.vendor_id = vendor_id if not vendor_id: status.type = StatusType.PARTIAL status.messages.append("Disk vendor id could not be found") - device_id = open(f"{path}/device/oemid").read().strip() + device_id = _read_sysfs(f"{path}/device/oemid") disk.device_id = device_id if not device_id: status.type = StatusType.PARTIAL status.messages.append("Disk device id could not be found") - size = open(f"{path}/size").read().strip() + size = _read_sysfs(f"{path}/size") size_in_bytes = int(size) * 512 disk.size = Megabyte(capacity=(size_in_bytes // 1024**2)) @@ -66,15 +72,15 @@ def _fetch_standard_disk_info(folder: str) -> tuple[DiskInfo, Status]: disk.identifier = folder.strip() - model = open(f"{path}/device/model").read().strip() + model = _read_sysfs(f"{path}/device/model") if model: disk.model = model else: status.type = StatusType.PARTIAL status.messages.append("Disk Model could not be found") - rotational = open(f"{path}/queue/rotational").read().strip() - removable = open(f"{path}/removable").read().strip() + rotational = _read_sysfs(f"{path}/queue/rotational") + removable = _read_sysfs(f"{path}/removable") disk.type = "Solid State Drive (SSD)" if rotational == "0" else "Hard Disk Drive (HDD)" disk.location = "Internal" if removable == "0" else "External" @@ -82,15 +88,15 @@ def _fetch_standard_disk_info(folder: str) -> tuple[DiskInfo, Status]: if "nvme" in folder: disk.connector = "PCIe" disk.type = "Non-Volatile Memory Express (NVMe)" - disk.device_id = open(f"{path}/device/device/device").read().strip() - disk.vendor_id = open(f"{path}/device/device/vendor").read().strip() + disk.device_id = _read_sysfs(f"{path}/device/device/device") + disk.vendor_id = _read_sysfs(f"{path}/device/device/vendor") elif "sd" in folder: disk.connector = "SCSI" - disk.vendor_id = open(f"{path}/device/vendor").read().strip() + disk.vendor_id = _read_sysfs(f"{path}/device/vendor") else: disk.connector = "Unknown" - size = open(f"{path}/size").read().strip() + size = _read_sysfs(f"{path}/size") size_in_bytes = int(size) * 512 disk.size = Megabyte(capacity=(size_in_bytes // 1024**2)) diff --git a/src/hwprobe/util/location_paths.py b/src/hwprobe/util/location_paths.py index eeca35e..7aa947c 100644 --- a/src/hwprobe/util/location_paths.py +++ b/src/hwprobe/util/location_paths.py @@ -171,7 +171,7 @@ def decode_uint32(raw_bytes: bytes) -> Optional[int]: """ try: return int.from_bytes(raw_bytes[:4], byteorder="little") - except: + except Exception: return None From d015e25d502f1b639ce609b7856bd32d258d6639 Mon Sep 17 00:00:00 2001 From: Mahasvan Date: Sat, 1 Aug 2026 17:58:53 +0530 Subject: [PATCH 10/10] test: HardwareManager tests for Mac and Linux; network tests for mac --- tests/core/linux/test_manager.py | 125 +++++++++++++++++++ tests/core/mac/test_manager.py | 115 ++++++++++++++++++ tests/core/mac/test_network.py | 199 +++++++++++++++++++++++++++++++ 3 files changed, 439 insertions(+) create mode 100644 tests/core/linux/test_manager.py create mode 100644 tests/core/mac/test_manager.py diff --git a/tests/core/linux/test_manager.py b/tests/core/linux/test_manager.py new file mode 100644 index 0000000..c8af40d --- /dev/null +++ b/tests/core/linux/test_manager.py @@ -0,0 +1,125 @@ +"""Tests for LinuxHardwareManager — verifies delegation and info aggregation. + +Only covers supported components: CPU, GPU, Memory, Network, Storage. +""" + +import pytest + +from hwprobe.core.linux.manager import LinuxHardwareManager +from hwprobe.models.cpu_models import CPUInfo +from hwprobe.models.gpu_models import GraphicsInfo +from hwprobe.models.info_models import LinuxHardwareInfo +from hwprobe.models.memory_models import MemoryInfo +from hwprobe.models.network_models import NetworkInfo +from hwprobe.models.storage_models import StorageInfo + + +@pytest.fixture +def mgr(): + return LinuxHardwareManager() + + +class TestLinuxHardwareManagerInit: + def test_init_creates_linux_hardware_info(self, mgr): + assert isinstance(mgr.info, LinuxHardwareInfo) + + @pytest.mark.parametrize( + "attr,expected_type", + [ + ("cpu", CPUInfo), + ("graphics", GraphicsInfo), + ("memory", MemoryInfo), + ("storage", StorageInfo), + ("network", NetworkInfo), + ], + ) + def test_init_populates_component(self, mgr, attr, expected_type): + assert isinstance(getattr(mgr.info, attr), expected_type) + + @pytest.mark.parametrize( + "attr", + ["graphics", "memory", "storage", "network"], + ) + def test_init_components_have_empty_modules(self, mgr, attr): + assert getattr(mgr.info, attr).modules == [] + + def test_init_cpu_has_no_name(self, mgr): + assert mgr.info.cpu.name is None + + +# ── fetch_* methods that delegate AND store on self.info ───────────────────── + +# (module_path, method_name, info_attr, fake_return) +_STORING_FETCHES = [ + ("hwprobe.core.linux.manager.fetch_cpu_info", "fetch_cpu_info", "cpu", CPUInfo(name="AMD Ryzen 9 7950X", vendor="AuthenticAMD")), + ("hwprobe.core.linux.manager.fetch_memory_info", "fetch_memory_info", "memory", MemoryInfo()), + ("hwprobe.core.linux.manager.fetch_storage_info", "fetch_storage_info", "storage", StorageInfo()), + ("hwprobe.core.linux.manager.fetch_graphics_info", "fetch_graphics_info", "graphics", GraphicsInfo()), +] + + +@pytest.mark.parametrize( + "module_path,method_name,info_attr,fake_return", + _STORING_FETCHES, + ids=[f[1] for f in _STORING_FETCHES], +) +def test_fetch_method_delegates_to_module_function(mgr, module_path, method_name, info_attr, fake_return): + with pytest.MonkeyPatch().context() as mp: + mp.setattr(module_path, lambda: fake_return) + result = getattr(mgr, method_name)() + assert result is fake_return + + +@pytest.mark.parametrize( + "module_path,method_name,info_attr,fake_return", + _STORING_FETCHES, + ids=[f[1] for f in _STORING_FETCHES], +) +def test_fetch_method_stores_result_on_info(mgr, module_path, method_name, info_attr, fake_return): + with pytest.MonkeyPatch().context() as mp: + mp.setattr(module_path, lambda: fake_return) + getattr(mgr, method_name)() + assert getattr(mgr.info, info_attr) is fake_return + + +# ── fetch_network_info — delegates but does NOT store on self.info ─────────── + + +def test_fetch_network_info_delegates_to_module_function(mgr): + fake_net = NetworkInfo() + with pytest.MonkeyPatch().context() as mp: + mp.setattr("hwprobe.core.linux.manager.fetch_network_info", lambda: fake_net) + result = mgr.fetch_network_info() + assert result is fake_net + + +# ── fetch_hardware_info — aggregates all sub-fetches ───────────────────────── + + +@pytest.fixture +def mgr_with_all_mocked(mgr): + """Manager with all fetch_* module functions patched.""" + mp = pytest.MonkeyPatch() + mp.setattr("hwprobe.core.linux.manager.fetch_cpu_info", lambda: CPUInfo(name="AMD Ryzen 9 7950X", vendor="AuthenticAMD")) + mp.setattr("hwprobe.core.linux.manager.fetch_graphics_info", lambda: GraphicsInfo()) + mp.setattr("hwprobe.core.linux.manager.fetch_memory_info", lambda: MemoryInfo()) + mp.setattr("hwprobe.core.linux.manager.fetch_storage_info", lambda: StorageInfo()) + mp.setattr("hwprobe.core.linux.manager.fetch_network_info", lambda: NetworkInfo()) + yield mgr + mp.undo() + + +def test_fetch_hardware_info_returns_self_info(mgr_with_all_mocked): + result = mgr_with_all_mocked.fetch_hardware_info() + assert result is mgr_with_all_mocked.info + + +def test_fetch_hardware_info_returns_linux_hardware_info(mgr_with_all_mocked): + result = mgr_with_all_mocked.fetch_hardware_info() + assert isinstance(result, LinuxHardwareInfo) + + +def test_fetch_hardware_info_populates_cpu(mgr_with_all_mocked): + result = mgr_with_all_mocked.fetch_hardware_info() + assert result.cpu.name == "AMD Ryzen 9 7950X" + assert result.cpu.vendor == "AuthenticAMD" diff --git a/tests/core/mac/test_manager.py b/tests/core/mac/test_manager.py new file mode 100644 index 0000000..c8327f0 --- /dev/null +++ b/tests/core/mac/test_manager.py @@ -0,0 +1,115 @@ +"""Tests for MacHardwareManager — verifies delegation and info aggregation. + +Only covers supported components: CPU, GPU, Memory, Network, Storage. +""" + +import pytest + +from hwprobe.core.mac.manager import MacHardwareManager +from hwprobe.models.cpu_models import CPUInfo +from hwprobe.models.gpu_models import GraphicsInfo +from hwprobe.models.info_models import MacHardwareInfo +from hwprobe.models.memory_models import MemoryInfo +from hwprobe.models.network_models import NetworkInfo +from hwprobe.models.storage_models import StorageInfo + + +@pytest.fixture +def mgr(): + return MacHardwareManager() + + +class TestMacHardwareManagerInit: + def test_init_creates_mac_hardware_info(self, mgr): + assert isinstance(mgr.info, MacHardwareInfo) + + @pytest.mark.parametrize( + "attr,expected_type", + [ + ("cpu", CPUInfo), + ("graphics", GraphicsInfo), + ("memory", MemoryInfo), + ("storage", StorageInfo), + ("network", NetworkInfo), + ], + ) + def test_init_populates_component(self, mgr, attr, expected_type): + assert isinstance(getattr(mgr.info, attr), expected_type) + + @pytest.mark.parametrize( + "attr", + ["graphics", "memory", "storage", "network"], + ) + def test_init_components_have_empty_modules(self, mgr, attr): + assert getattr(mgr.info, attr).modules == [] + + def test_init_cpu_has_no_name(self, mgr): + assert mgr.info.cpu.name is None + + +# ── Individual fetch_* methods that delegate and store on self.info ────────── + +# (module_path, method_name, info_attr, fake_return) +_DELEGATING_FETCHES = [ + ("hwprobe.core.mac.manager.fetch_cpu_info", "fetch_cpu_info", "cpu", CPUInfo(name="Apple M3")), + ("hwprobe.core.mac.manager.fetch_memory_info", "fetch_memory_info", "memory", MemoryInfo()), + ("hwprobe.core.mac.manager.fetch_storage_info", "fetch_storage_info", "storage", StorageInfo()), + ("hwprobe.core.mac.manager.fetch_graphics_info", "fetch_graphics_info", "graphics", GraphicsInfo()), + ("hwprobe.core.mac.manager.fetch_network_info", "fetch_network_info", "network", NetworkInfo()), +] + + +@pytest.mark.parametrize( + "module_path,method_name,info_attr,fake_return", + _DELEGATING_FETCHES, + ids=[f[1] for f in _DELEGATING_FETCHES], +) +def test_fetch_method_delegates_to_module_function(mgr, module_path, method_name, info_attr, fake_return): + with pytest.MonkeyPatch().context() as mp: + mp.setattr(module_path, lambda: fake_return) + result = getattr(mgr, method_name)() + assert result is fake_return + + +@pytest.mark.parametrize( + "module_path,method_name,info_attr,fake_return", + _DELEGATING_FETCHES, + ids=[f[1] for f in _DELEGATING_FETCHES], +) +def test_fetch_method_stores_result_on_info(mgr, module_path, method_name, info_attr, fake_return): + with pytest.MonkeyPatch().context() as mp: + mp.setattr(module_path, lambda: fake_return) + getattr(mgr, method_name)() + assert getattr(mgr.info, info_attr) is fake_return + + +# ── fetch_hardware_info — aggregates all sub-fetches ───────────────────────── + + +@pytest.fixture +def mgr_with_all_mocked(mgr): + """Manager with all fetch_* module functions patched.""" + mp = pytest.MonkeyPatch() + mp.setattr("hwprobe.core.mac.manager.fetch_cpu_info", lambda: CPUInfo(name="Apple M3", vendor="Apple")) + mp.setattr("hwprobe.core.mac.manager.fetch_graphics_info", lambda: GraphicsInfo()) + mp.setattr("hwprobe.core.mac.manager.fetch_memory_info", lambda: MemoryInfo()) + mp.setattr("hwprobe.core.mac.manager.fetch_storage_info", lambda: StorageInfo()) + mp.setattr("hwprobe.core.mac.manager.fetch_network_info", lambda: NetworkInfo()) + yield mgr + mp.undo() + + +def test_fetch_hardware_info_returns_self_info(mgr_with_all_mocked): + result = mgr_with_all_mocked.fetch_hardware_info() + assert result is mgr_with_all_mocked.info + + +def test_fetch_hardware_info_returns_mac_hardware_info(mgr_with_all_mocked): + result = mgr_with_all_mocked.fetch_hardware_info() + assert isinstance(result, MacHardwareInfo) + + +def test_fetch_hardware_info_populates_cpu(mgr_with_all_mocked): + result = mgr_with_all_mocked.fetch_hardware_info() + assert result.cpu.name == "Apple M3" + assert result.cpu.vendor == "Apple" diff --git a/tests/core/mac/test_network.py b/tests/core/mac/test_network.py index bbbc4fe..066f2d1 100644 --- a/tests/core/mac/test_network.py +++ b/tests/core/mac/test_network.py @@ -1,4 +1,5 @@ import plistlib +import subprocess from unittest.mock import MagicMock, patch import pytest @@ -13,6 +14,7 @@ fetch_network_info, ) from hwprobe.models.network_models import NetworkInfo, NICInfo +from hwprobe.models.status_models import StatusType # ── helpers ────────────────────────────────────────────────────────────────── @@ -110,6 +112,37 @@ def _make_brcm4331_ioreg_entry( } +def _make_wlan_driver_ioreg_entry( + chipset="BCM4389", + vendor="Apple", + bsd_name="en0", +): + """AppleWLANDriver entry as seen on Wi-Fi 7 / M5 series Macs.""" + return { + "IORegistryEntryName": "AppleWLANDriver", + "AirshipDeviceCriteria": { + "Chipset": chipset, + "Vendor": vendor, + }, + "IORegistryEntryChildren": [ + { + "IORegistryEntryName": "AppleWLANInterfaceSTA", + "IORegistryEntryChildren": [ + { + "IORegistryEntryName": "IOSkywalkLegacyEthernet", + "IORegistryEntryChildren": [ + { + "IOObjectClass": "IOSkywalkLegacyEthernetInterface", + "IORegistryEntryName": bsd_name, + } + ], + } + ], + } + ], + } + + # ── _fetch_controllers ─────────────────────────────────────────────────────── @@ -141,6 +174,11 @@ def test_subprocess_failure_propagates(self, mock_run): with pytest.raises(FileNotFoundError): _fetch_controllers() + @patch("hwprobe.core.mac.network.subprocess.run") + def test_called_process_error_returns_empty(self, mock_run): + mock_run.side_effect = subprocess.CalledProcessError(1, "ipconfig") + assert _fetch_controllers() == [] + # ── _fetch_ethernet_details ────────────────────────────────────────────────── @@ -198,6 +236,11 @@ def test_subprocess_failure_propagates(self, mock_run): with pytest.raises(FileNotFoundError): _fetch_ethernet_details() + @patch("hwprobe.core.mac.network.subprocess.run") + def test_called_process_error_returns_empty(self, mock_run): + mock_run.side_effect = subprocess.CalledProcessError(1, "system_profiler") + assert _fetch_ethernet_details() == {} + # ── _find_child ────────────────────────────────────────────────────────────── @@ -278,6 +321,26 @@ def test_returns_none_when_legacy_interface_missing(self): def test_returns_none_when_children_key_absent(self): assert _get_bsd_interface_apple_silicon({}) is None + def test_unknown_driver_falls_back_to_bcm_then_wlan(self): + """Unknown driver name tries BCM path first, then WLAN path.""" + # BCM path matches → returns en0 + item = _make_apple_silicon_ioreg_entry(bsd_name="en0") + assert _get_bsd_interface_apple_silicon(item, driver="UnknownDriver") == "en0" + + def test_unknown_driver_falls_back_to_wlan_when_bcm_fails(self): + """Unknown driver: BCM path fails, WLAN path succeeds.""" + item = _make_wlan_driver_ioreg_entry(bsd_name="en1") + assert _get_bsd_interface_apple_silicon(item, driver="UnknownDriver") == "en1" + + def test_unknown_driver_returns_none_when_both_paths_fail(self): + """Unknown driver: both BCM and WLAN paths fail.""" + assert _get_bsd_interface_apple_silicon({}, driver="UnknownDriver") is None + + def test_wlan_driver_path_resolves(self): + """AppleWLANDriver traversal path resolves BSD interface name.""" + item = _make_wlan_driver_ioreg_entry(bsd_name="en1") + assert _get_bsd_interface_apple_silicon(item, driver="AppleWLANDriver") == "en1" + # ── _fetch_airport_details ─────────────────────────────────────────────────── @@ -354,6 +417,17 @@ def test_apple_silicon_bcm_wlan_core(self, mock_run): assert result["en0"].vendor_id == "0x14e4" assert result["en0"].device_id == "0x4488" + @patch("hwprobe.core.mac.network.subprocess.run") + def test_apple_silicon_bcm_apple_manufacturer(self, mock_run): + """AppleBCMWLANCore with subsystem-vendor-id 0x106b sets manufacturer to Apple.""" + entry = _make_apple_silicon_ioreg_entry(bsd_name="en0") + entry["ModuleDictionary"]["subsystem-vendor-id"] = 4203 # 0x106b + plist_data = _make_ioreg_plist([entry]) + mock_run.return_value = MagicMock(stdout=plist_data) + + result = _fetch_airport_details() + assert result["en0"].manufacturer == "Apple" + @patch("hwprobe.core.mac.network.subprocess.run") def test_apple_silicon_no_bsd_interface_skipped(self, mock_run): """Apple Silicon entry with no resolvable BSD interface is not added.""" @@ -368,6 +442,92 @@ def test_apple_silicon_no_bsd_interface_skipped(self, mock_run): result = _fetch_airport_details() assert result == {} + @patch("hwprobe.core.mac.network.subprocess.run") + def test_wlan_driver_with_chipset_and_vendor(self, mock_run): + """AppleWLANDriver entry with chipset and vendor is parsed correctly.""" + plist_data = _make_ioreg_plist( + [_make_wlan_driver_ioreg_entry(chipset="BCM4389", vendor="Apple", bsd_name="en0")] + ) + mock_run.return_value = MagicMock(stdout=plist_data) + + result = _fetch_airport_details() + assert "en0" in result + assert result["en0"].manufacturer == "Apple (Apple)" + assert result["en0"].name == "Wi-Fi (BCM4389 chipset)" + + @patch("hwprobe.core.mac.network.subprocess.run") + def test_wlan_driver_without_vendor(self, mock_run): + """AppleWLANDriver with no vendor falls back to plain 'Apple'.""" + entry = _make_wlan_driver_ioreg_entry(vendor=None, bsd_name="en0") + # Remove the Vendor key entirely to test the `else` branch + del entry["AirshipDeviceCriteria"]["Vendor"] + plist_data = _make_ioreg_plist([entry]) + mock_run.return_value = MagicMock(stdout=plist_data) + + result = _fetch_airport_details() + assert result["en0"].manufacturer == "Apple" + assert result["en0"].name == "Wi-Fi (BCM4389 chipset)" + + @patch("hwprobe.core.mac.network.subprocess.run") + def test_wlan_driver_without_chipset(self, mock_run): + """AppleWLANDriver with no chipset leaves name as None.""" + entry = _make_wlan_driver_ioreg_entry(chipset=None, bsd_name="en0") + del entry["AirshipDeviceCriteria"]["Chipset"] + plist_data = _make_ioreg_plist([entry]) + mock_run.return_value = MagicMock(stdout=plist_data) + + result = _fetch_airport_details() + assert result["en0"].name is None + assert result["en0"].manufacturer == "Apple (Apple)" + + @patch("hwprobe.core.mac.network.subprocess.run") + def test_wlan_driver_missing_airship_criteria(self, mock_run): + """AppleWLANDriver with no AirshipDeviceCriteria key doesn't crash.""" + entry = { + "IORegistryEntryName": "AppleWLANDriver", + "IORegistryEntryChildren": _make_wlan_driver_ioreg_entry()["IORegistryEntryChildren"], + } + plist_data = _make_ioreg_plist([entry]) + mock_run.return_value = MagicMock(stdout=plist_data) + + result = _fetch_airport_details() + assert "en0" in result + assert result["en0"].manufacturer == "Apple" + assert result["en0"].name is None + + @patch("hwprobe.core.mac.network.subprocess.run") + def test_wlan_driver_no_bsd_interface_skipped(self, mock_run): + """AppleWLANDriver with no resolvable BSD interface is skipped.""" + entry = _make_wlan_driver_ioreg_entry(bsd_name="en0") + entry["IORegistryEntryChildren"] = [] # break the traversal + plist_data = _make_ioreg_plist([entry]) + mock_run.return_value = MagicMock(stdout=plist_data) + + result = _fetch_airport_details() + assert result == {} + + @patch("hwprobe.core.mac.network.subprocess.run") + def test_called_process_error_returns_empty(self, mock_run): + mock_run.side_effect = subprocess.CalledProcessError(1, "ioreg") + assert _fetch_airport_details() == {} + + @patch("hwprobe.core.mac.network.subprocess.run") + def test_brcm_nic_no_regex_match_skipped(self, mock_run): + """AirPort_BrcmNIC with malformed IONameMatched is skipped (no crash).""" + entry = { + "IORegistryEntryName": "AirPort_BrcmNIC", + "IONameMatched": "invalid-format", + "IOModel": "AirPort Extreme", + "IORegistryEntryChildren": [ + {"IOObjectClass": "AirPort_BrcmNIC_Interface", "IORegistryEntryName": "en1"} + ], + } + plist_data = _make_ioreg_plist([entry]) + mock_run.return_value = MagicMock(stdout=plist_data) + + result = _fetch_airport_details() + assert result == {} + @patch("hwprobe.core.mac.network.subprocess.run") def test_brcm4331_driver(self, mock_run): """AirPort_Brcm4331 entry is parsed correctly on older Intel Macs.""" @@ -545,6 +705,37 @@ def test_single_wifi_nic(self, mock_run, mock_air): assert m.type == "AirPort" assert m.vendor_id == "0x14e4" + @patch("hwprobe.core.mac.network._fetch_airport_details") + @patch("hwprobe.core.mac.network.subprocess.run") + def test_wifi_nic_enriched_with_manufacturer_and_name(self, mock_run, mock_air): + """AirPort module gets manufacturer and name from airport_info.""" + network_plist = _make_network_plist( + [ + { + "interface": "en1", + "_name": "Wi-Fi", + "Ethernet": {"MAC Address": "11:22:33:44:55:66"}, + "type": "AirPort", + } + ] + ) + mock_run.return_value = MagicMock(stdout=network_plist) + mock_air.return_value = { + "en1": NICInfo( + vendor_id="0x14e4", + device_id="0x4331", + manufacturer="Broadcom", + name="BCM4389", + ) + } + + result = _fetch_system_profiler_details(["en1"]) + m = result.modules[0] + assert m.manufacturer == "Broadcom" + assert m.name == "BCM4389" + assert m.vendor_id == "0x14e4" + assert m.device_id == "0x4331" + @patch("hwprobe.core.mac.network.subprocess.run") def test_interface_not_in_valid_list_is_skipped(self, mock_run): network_plist = _make_network_plist( @@ -624,6 +815,14 @@ def test_mixed_ethernet_and_wifi(self, mock_run, mock_air, mock_eth): result = _fetch_system_profiler_details(["en0", "en1"]) assert len(result.modules) == 2 + @patch("hwprobe.core.mac.network.subprocess.run") + def test_called_process_error_returns_failed_status(self, mock_run): + mock_run.side_effect = subprocess.CalledProcessError(1, "system_profiler") + result = _fetch_system_profiler_details(["en0"]) + assert result.status.type == StatusType.FAILED + assert any("system_profiler" in m for m in result.status.messages) + assert result.modules == [] + # ── Missing _items key handled gracefully ─────────────────────────────────