diff --git a/src/winml/modelkit/commands/sys.py b/src/winml/modelkit/commands/sys.py index 161e7b1b0..67a1d305c 100644 --- a/src/winml/modelkit/commands/sys.py +++ b/src/winml/modelkit/commands/sys.py @@ -25,6 +25,7 @@ import json import logging +import os import platform import sys from typing import Any @@ -53,26 +54,31 @@ def _get_python_info() -> dict[str, Any]: def _get_platform_info() -> dict[str, Any]: """Gather OS and platform information.""" - system = platform.system() release = platform.release() - # For Windows, use OS class for accurate Windows 11 detection - # platform.release() may incorrectly report '10' on some Python versions - if system == "Windows": - try: - os_info = OS.get() - # Only override if it's actually Windows 11 - # Otherwise keep the original platform.release() value - if os_info.is_windows_11(): - release = "11" - except Exception: - # Fallback to platform.release() if OS detection fails - pass + # platform.release() may incorrectly report '10' for Windows 11 on some + # Python versions; OS class queries the build number for accurate detection. + try: + if OS.get().is_windows_11(): + release = "11" + except Exception: + # Fallback to platform.release() if OS detection fails + pass + + # platform.machine() reports the running Python process architecture. + # On ARM64 Windows running an x64 Python under WOW64 emulation, + # PROCESSOR_ARCHITEW6432 holds the true host arch; otherwise + # PROCESSOR_ARCHITECTURE matches the host. + machine = ( + os.environ.get("PROCESSOR_ARCHITEW6432") + or os.environ.get("PROCESSOR_ARCHITECTURE") + or platform.machine() + ) return { - "system": system, + "system": platform.system(), "release": release, - "machine": platform.machine(), + "machine": machine, "processor": platform.processor() or "Unknown", } diff --git a/tests/unit/sysinfo/test_sysinfo.py b/tests/unit/sysinfo/test_sysinfo.py index 2b8da839c..fdd7c8b14 100644 --- a/tests/unit/sysinfo/test_sysinfo.py +++ b/tests/unit/sysinfo/test_sysinfo.py @@ -9,6 +9,7 @@ from __future__ import annotations +import os from unittest.mock import MagicMock, patch @@ -128,51 +129,108 @@ def test_windows_detection_fallback_on_exception( assert result["release"] == "10" assert result["machine"] == "AMD64" + @patch("winml.modelkit.commands.sys.OS") @patch("winml.modelkit.commands.sys.platform") - def test_non_windows_platform(self, mock_platform: MagicMock) -> None: - """Test non-Windows platforms pass through unchanged.""" + def test_processor_unknown_fallback( + self, mock_platform: MagicMock, mock_os_class: MagicMock + ) -> None: + """Test processor defaults to 'Unknown' when empty.""" from winml.modelkit.commands.sys import _get_platform_info - # Setup mocks for Linux - mock_platform.system.return_value = "Linux" - mock_platform.release.return_value = "5.15.0" - mock_platform.machine.return_value = "x86_64" - mock_platform.processor.return_value = "x86_64" + mock_platform.system.return_value = "Windows" + mock_platform.release.return_value = "10" + mock_platform.machine.return_value = "AMD64" + mock_platform.processor.return_value = "" # Empty string - result = _get_platform_info() + mock_os_instance = MagicMock() + mock_os_instance.is_windows_11.return_value = False + mock_os_class.get.return_value = mock_os_instance + + with patch.dict(os.environ, {"PROCESSOR_ARCHITECTURE": "AMD64"}, clear=True): + result = _get_platform_info() + + assert result["processor"] == "Unknown" - assert result["system"] == "Linux" - assert result["release"] == "5.15.0" - assert result["machine"] == "x86_64" +class TestWindowsHostArchitecture: + """Test that Windows machine architecture reports the host, not the Python process arch. + + Regression coverage for https://github.com/microsoft/WinML-ModelKit/issues/58: + `platform.machine()` returns the running Python process architecture, which is + incorrect on ARM64 Windows when an x64 Python runs under WOW64 emulation. + """ + + @patch("winml.modelkit.commands.sys.OS") @patch("winml.modelkit.commands.sys.platform") - def test_macos_platform(self, mock_platform: MagicMock) -> None: - """Test macOS platforms pass through unchanged.""" + def test_native_arm64_python_reports_arm64( + self, mock_platform: MagicMock, mock_os_class: MagicMock + ) -> None: + """Native ARM64 Python on ARM64 Windows must report ARM64.""" from winml.modelkit.commands.sys import _get_platform_info - # Setup mocks for macOS - mock_platform.system.return_value = "Darwin" - mock_platform.release.return_value = "21.6.0" - mock_platform.machine.return_value = "arm64" - mock_platform.processor.return_value = "arm" + mock_platform.system.return_value = "Windows" + mock_platform.release.return_value = "10" + mock_platform.machine.return_value = "ARM64" + mock_platform.processor.return_value = "ARMv8 (64-bit)" - result = _get_platform_info() + mock_os_instance = MagicMock() + mock_os_instance.is_windows_11.return_value = True + mock_os_class.get.return_value = mock_os_instance + + with patch.dict(os.environ, {"PROCESSOR_ARCHITECTURE": "ARM64"}, clear=True): + result = _get_platform_info() - assert result["system"] == "Darwin" - assert result["release"] == "21.6.0" - assert result["machine"] == "arm64" + assert result["machine"] == "ARM64" + @patch("winml.modelkit.commands.sys.OS") @patch("winml.modelkit.commands.sys.platform") - def test_processor_unknown_fallback(self, mock_platform: MagicMock) -> None: - """Test processor defaults to 'Unknown' when empty.""" + def test_x64_emulated_python_on_arm64_reports_arm64( + self, mock_platform: MagicMock, mock_os_class: MagicMock + ) -> None: + """x64-emulated Python on ARM64 Windows must report ARM64, not AMD64. + + Under WOW64 emulation, PROCESSOR_ARCHITECTURE reflects the emulated + process arch (AMD64) and PROCESSOR_ARCHITEW6432 holds the true host + arch (ARM64). The host arch is what users want to see. + """ from winml.modelkit.commands.sys import _get_platform_info - # Setup mocks - mock_platform.system.return_value = "Linux" - mock_platform.release.return_value = "5.15.0" - mock_platform.machine.return_value = "x86_64" - mock_platform.processor.return_value = "" # Empty string + mock_platform.system.return_value = "Windows" + mock_platform.release.return_value = "11" + mock_platform.machine.return_value = "AMD64" + mock_platform.processor.return_value = "Intel64 Family 6" - result = _get_platform_info() + mock_os_instance = MagicMock() + mock_os_instance.is_windows_11.return_value = True + mock_os_class.get.return_value = mock_os_instance - assert result["processor"] == "Unknown" + with patch.dict( + os.environ, + {"PROCESSOR_ARCHITECTURE": "AMD64", "PROCESSOR_ARCHITEW6432": "ARM64"}, + clear=True, + ): + result = _get_platform_info() + + assert result["machine"] == "ARM64" + + @patch("winml.modelkit.commands.sys.OS") + @patch("winml.modelkit.commands.sys.platform") + def test_native_amd64_python_reports_amd64( + self, mock_platform: MagicMock, mock_os_class: MagicMock + ) -> None: + """Native AMD64 Python on AMD64 Windows must still report AMD64.""" + from winml.modelkit.commands.sys import _get_platform_info + + mock_platform.system.return_value = "Windows" + mock_platform.release.return_value = "11" + mock_platform.machine.return_value = "AMD64" + mock_platform.processor.return_value = "Intel64 Family 6" + + mock_os_instance = MagicMock() + mock_os_instance.is_windows_11.return_value = True + mock_os_class.get.return_value = mock_os_instance + + with patch.dict(os.environ, {"PROCESSOR_ARCHITECTURE": "AMD64"}, clear=True): + result = _get_platform_info() + + assert result["machine"] == "AMD64"