EU-Utility/plugins/integration_tests/platform_compat/plugin.py

849 lines
30 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
EU-Utility Integration Test - Platform Compatibility
=====================================================
Tests cross-platform compatibility:
- Windows-specific features
- Linux compatibility
- Path handling differences
- File locking mechanisms
- Process management
Author: Integration Tester
Version: 1.0.0
"""
import sys
import os
import platform
import subprocess
import threading
import tempfile
from pathlib import Path
from typing import Dict, Any, List, Tuple, Optional
from dataclasses import dataclass, asdict
from datetime import datetime
from plugins.base_plugin import BasePlugin
from PyQt6.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton,
QTextEdit, QTableWidget, QTableWidgetItem, QHeaderView,
QGroupBox, QProgressBar, QTabWidget
)
from PyQt6.QtCore import Qt
@dataclass
class PlatformTest:
"""Platform compatibility test case."""
name: str
category: str # 'path', 'file', 'process', 'system'
description: str
windows_only: bool = False
linux_only: bool = False
class PlatformCompatibilityTester(BasePlugin):
"""Plugin for testing cross-platform compatibility."""
name = "Platform Compatibility Tester"
version = "1.0.0"
author = "Integration Tester"
description = "Test cross-platform compatibility and platform-specific features"
# Platform info
IS_WINDOWS = sys.platform == 'win32'
IS_LINUX = sys.platform == 'linux'
IS_MAC = sys.platform == 'darwin'
# Test cases
TEST_CASES = [
PlatformTest("Path Separator", "path", "Test path separator handling"),
PlatformTest("Path Resolution", "path", "Test path resolution"),
PlatformTest("Unicode Paths", "path", "Test Unicode in paths"),
PlatformTest("Long Paths", "path", "Test long path handling (Windows)", windows_only=True),
PlatformTest("UNC Paths", "path", "Test UNC path handling (Windows)", windows_only=True),
PlatformTest("File Locking - fcntl", "file", "Test fcntl file locking (Linux/Mac)", linux_only=True),
PlatformTest("File Locking - portalocker", "file", "Test portalocker file locking (Windows)"),
PlatformTest("File Permissions", "file", "Test file permission handling"),
PlatformTest("Temp Directory", "file", "Test temp directory access"),
PlatformTest("Process List", "process", "Test process enumeration"),
PlatformTest("Window Enumeration", "process", "Test window enumeration (Windows)", windows_only=True),
PlatformTest("CPU Info", "system", "Test CPU information retrieval"),
PlatformTest("Memory Info", "system", "Test memory information retrieval"),
PlatformTest("Environment Variables", "system", "Test environment variable handling"),
]
def initialize(self):
"""Initialize the tester."""
self.log_info("Platform Compatibility Tester initialized")
self._test_results: List[Dict] = []
def get_ui(self) -> QWidget:
"""Create the plugin UI."""
widget = QWidget()
layout = QVBoxLayout(widget)
# Title
title = QLabel("Platform Compatibility Tester")
title.setStyleSheet("font-size: 16px; font-weight: bold;")
layout.addWidget(title)
# Platform info banner
self._create_platform_banner(layout)
# Tabs
tabs = QTabWidget()
# Overview tab
tabs.addTab(self._create_overview_tab(), "Overview")
# Path Tests tab
tabs.addTab(self._create_path_tab(), "Path Tests")
# File Tests tab
tabs.addTab(self._create_file_tab(), "File Tests")
# System Tests tab
tabs.addTab(self._create_system_tab(), "System Tests")
# Test Results tab
tabs.addTab(self._create_results_tab(), "Test Results")
layout.addWidget(tabs)
return widget
def _create_platform_banner(self, parent_layout):
"""Create the platform info banner."""
banner = QGroupBox("Current Platform")
banner_layout = QHBoxLayout(banner)
# Platform icon/color
if self.IS_WINDOWS:
platform_text = "🪟 Windows"
color = "#0078D4"
elif self.IS_LINUX:
platform_text = "🐧 Linux"
color = "#FCC624"
elif self.IS_MAC:
platform_text = "🍎 macOS"
color = "#999999"
else:
platform_text = f"{sys.platform}"
color = "#888888"
# Platform label
platform_label = QLabel(f"Platform: {platform_text}")
platform_label.setStyleSheet(f"color: {color}; font-weight: bold;")
banner_layout.addWidget(platform_label)
# Python version
python_label = QLabel(f"Python: {platform.python_version()}")
banner_layout.addWidget(python_label)
# Architecture
arch_label = QLabel(f"Arch: {platform.machine()}")
banner_layout.addWidget(arch_label)
# Processor info
proc_label = QLabel(f"Processor: {platform.processor() or 'Unknown'}")
banner_layout.addWidget(proc_label)
banner_layout.addStretch()
parent_layout.addWidget(banner)
def _create_overview_tab(self) -> QWidget:
"""Create the overview tab."""
widget = QWidget()
layout = QVBoxLayout(widget)
# Run all tests button
run_btn = QPushButton("Run All Compatibility Tests")
run_btn.setStyleSheet("font-size: 14px; padding: 10px;")
run_btn.clicked.connect(self._run_all_tests)
layout.addWidget(run_btn)
# Platform details
details = QTextEdit()
details.setReadOnly(True)
details.setHtml(self._get_platform_details_html())
layout.addWidget(details)
# Feature support matrix
layout.addWidget(QLabel("Feature Support Matrix:"))
matrix = QTextEdit()
matrix.setReadOnly(True)
matrix.setMaximumHeight(200)
matrix.setHtml(self._get_feature_matrix_html())
layout.addWidget(matrix)
return widget
def _create_path_tab(self) -> QWidget:
"""Create the path tests tab."""
widget = QWidget()
layout = QVBoxLayout(widget)
layout.addWidget(QLabel("Path Handling Tests"))
# Path test results
self.path_results = QTextEdit()
self.path_results.setReadOnly(True)
layout.addWidget(self.path_results)
# Test buttons
btn_layout = QHBoxLayout()
test_sep_btn = QPushButton("Test Separators")
test_sep_btn.clicked.connect(self._test_path_separators)
btn_layout.addWidget(test_sep_btn)
test_resolve_btn = QPushButton("Test Resolution")
test_resolve_btn.clicked.connect(self._test_path_resolution)
btn_layout.addWidget(test_resolve_btn)
test_unicode_btn = QPushButton("Test Unicode")
test_unicode_btn.clicked.connect(self._test_unicode_paths)
btn_layout.addWidget(test_unicode_btn)
if self.IS_WINDOWS:
test_long_btn = QPushButton("Test Long Paths")
test_long_btn.clicked.connect(self._test_long_paths)
btn_layout.addWidget(test_long_btn)
layout.addLayout(btn_layout)
return widget
def _create_file_tab(self) -> QWidget:
"""Create the file tests tab."""
widget = QWidget()
layout = QVBoxLayout(widget)
layout.addWidget(QLabel("File Operations Tests"))
# File locking section
locking_group = QGroupBox("File Locking Tests")
locking_layout = QVBoxLayout(locking_group)
self.locking_results = QTextEdit()
self.locking_results.setReadOnly(True)
self.locking_results.setMaximumHeight(150)
locking_layout.addWidget(self.locking_results)
lock_btn_layout = QHBoxLayout()
test_fcntl_btn = QPushButton("Test fcntl")
test_fcntl_btn.clicked.connect(self._test_fcntl_locking)
test_fcntl_btn.setEnabled(self.IS_LINUX or self.IS_MAC)
lock_btn_layout.addWidget(test_fcntl_btn)
test_portalocker_btn = QPushButton("Test portalocker")
test_portalocker_btn.clicked.connect(self._test_portalocker_locking)
lock_btn_layout.addWidget(test_portalocker_btn)
test_threading_btn = QPushButton("Test Threading Lock")
test_threading_btn.clicked.connect(self._test_threading_lock)
lock_btn_layout.addWidget(test_threading_btn)
locking_layout.addLayout(lock_btn_layout)
layout.addWidget(locking_group)
# Permissions section
perm_group = QGroupBox("Permission Tests")
perm_layout = QVBoxLayout(perm_group)
self.perm_results = QTextEdit()
self.perm_results.setReadOnly(True)
self.perm_results.setMaximumHeight(100)
perm_layout.addWidget(self.perm_results)
test_perm_btn = QPushButton("Test File Permissions")
test_perm_btn.clicked.connect(self._test_file_permissions)
perm_layout.addWidget(test_perm_btn)
layout.addWidget(perm_group)
layout.addStretch()
return widget
def _create_system_tab(self) -> QWidget:
"""Create the system tests tab."""
widget = QWidget()
layout = QVBoxLayout(widget)
layout.addWidget(QLabel("System Information Tests"))
# System info display
self.system_info = QTextEdit()
self.system_info.setReadOnly(True)
layout.addWidget(self.system_info)
# Test buttons
btn_layout = QHBoxLayout()
cpu_btn = QPushButton("Get CPU Info")
cpu_btn.clicked.connect(self._test_cpu_info)
btn_layout.addWidget(cpu_btn)
mem_btn = QPushButton("Get Memory Info")
mem_btn.clicked.connect(self._test_memory_info)
btn_layout.addWidget(mem_btn)
env_btn = QPushButton("Test Environment")
env_btn.clicked.connect(self._test_environment)
btn_layout.addWidget(env_btn)
if self.IS_WINDOWS:
window_btn = QPushButton("Test Window Enum")
window_btn.clicked.connect(self._test_window_enumeration)
btn_layout.addWidget(window_btn)
layout.addLayout(btn_layout)
return widget
def _create_results_tab(self) -> QWidget:
"""Create the results tab."""
widget = QWidget()
layout = QVBoxLayout(widget)
layout.addWidget(QLabel("Test Results"))
# Results table
self.results_table = QTableWidget()
self.results_table.setColumnCount(5)
self.results_table.setHorizontalHeaderLabels([
"Test Name", "Category", "Status", "Details", "Platform"
])
self.results_table.horizontalHeader().setSectionResizeMode(3, QHeaderView.ResizeMode.Stretch)
layout.addWidget(self.results_table)
# Summary
self.results_summary = QLabel("No tests run yet")
layout.addWidget(self.results_summary)
# Export button
export_btn = QPushButton("Export Results")
export_btn.clicked.connect(self._export_results)
layout.addWidget(export_btn)
return widget
def _get_platform_details_html(self) -> str:
"""Get platform details as HTML."""
details = []
details.append("<h3>Platform Details</h3>")
details.append("<table border='1' cellpadding='5'>")
info = [
("System", platform.system()),
("Release", platform.release()),
("Version", platform.version()),
("Machine", platform.machine()),
("Processor", platform.processor() or 'Unknown'),
("Platform", platform.platform()),
("Python Version", platform.python_version()),
("Python Implementation", platform.python_implementation()),
("Python Compiler", platform.python_compiler()),
("Default Encoding", sys.getdefaultencoding()),
("Filesystem Encoding", sys.getfilesystemencoding()),
("Max Unicode", f"U+{sys.maxunicode:X}"),
("Executable", sys.executable),
("Prefix", sys.prefix),
]
for key, value in info:
details.append(f"<tr><td><b>{key}</b></td><td>{value}</td></tr>")
details.append("</table>")
return "\n".join(details)
def _get_feature_matrix_html(self) -> str:
"""Get feature support matrix as HTML."""
matrix = []
matrix.append("<table border='1' cellpadding='5'>")
matrix.append("<tr><th>Feature</th><th>Windows</th><th>Linux</th><th>macOS</th></tr>")
features = [
("Window Manager", "", "", ""),
("Native Hotkeys", "", "⚠️ Limited", "⚠️ Limited"),
("Global Hotkeys", "", "", ""),
("File Locking (fcntl)", "", "", ""),
("File Locking (portalocker)", "", "⚠️ Optional", "⚠️ Optional"),
("OCR (EasyOCR)", "", "", ""),
("OCR (Tesseract)", "", "", ""),
("Discord Webhooks", "", "", ""),
("MQTT", "", "", ""),
("WebSocket", "", "", ""),
("Long Paths (>260)", "✅*", "", ""),
]
for feature, win, lin, mac in features:
matrix.append(f"<tr><td>{feature}</td><td>{win}</td><td>{lin}</td><td>{mac}</td></tr>")
matrix.append("</table>")
matrix.append("<p>* Requires Windows 10 1607+ with long path support enabled</p>")
return "\n".join(matrix)
def _run_all_tests(self):
"""Run all compatibility tests."""
self._test_results.clear()
# Clear and setup results table
self.results_table.setRowCount(0)
for test in self.TEST_CASES:
# Skip platform-specific tests that don't apply
if test.windows_only and not self.IS_WINDOWS:
continue
if test.linux_only and not self.IS_LINUX:
continue
result = self._run_single_test(test)
self._test_results.append(result)
self._add_result_to_table(result)
self._update_results_summary()
def _run_single_test(self, test: PlatformTest) -> Dict:
"""Run a single test."""
result = {
"name": test.name,
"category": test.category,
"description": test.description,
"success": False,
"details": "",
"platform": "Windows" if test.windows_only else ("Linux" if test.linux_only else "All")
}
try:
if test.category == "path":
result.update(self._run_path_test(test))
elif test.category == "file":
result.update(self._run_file_test(test))
elif test.category == "process":
result.update(self._run_process_test(test))
elif test.category == "system":
result.update(self._run_system_test(test))
except Exception as e:
result["details"] = f"Error: {str(e)}"
return result
def _run_path_test(self, test: PlatformTest) -> Dict:
"""Run path-related tests."""
result = {"success": True, "details": ""}
if test.name == "Path Separator":
sep = os.sep
alt_sep = os.altsep
result["details"] = f"Primary: '{sep}', Alt: {repr(alt_sep)}"
elif test.name == "Path Resolution":
test_path = Path("~").expanduser()
result["details"] = f"Home resolved to: {test_path}"
elif test.name == "Unicode Paths":
# Test creating a path with Unicode
test_dir = Path(tempfile.gettempdir()) / "EU_Utility_测试"
result["details"] = f"Unicode path: {test_dir}"
elif test.name == "Long Paths":
if self.IS_WINDOWS:
# Windows long path test
result["details"] = "Long path support varies by Windows version"
return result
def _run_file_test(self, test: PlatformTest) -> Dict:
"""Run file-related tests."""
result = {"success": True, "details": ""}
if test.name == "File Locking - fcntl":
if self.IS_LINUX or self.IS_MAC:
try:
import fcntl
result["details"] = "fcntl module available"
except ImportError:
result["success"] = False
result["details"] = "fcntl not available"
else:
result["success"] = False
result["details"] = "Not applicable on Windows"
elif test.name == "File Locking - portalocker":
try:
import portalocker
result["details"] = "portalocker available"
except ImportError:
result["success"] = False
result["details"] = "portalocker not installed"
elif test.name == "File Permissions":
test_file = Path(tempfile.mktemp())
test_file.touch()
try:
mode = test_file.stat().st_mode
result["details"] = f"File mode: {oct(mode)}"
finally:
test_file.unlink()
return result
def _run_process_test(self, test: PlatformTest) -> Dict:
"""Run process-related tests."""
result = {"success": True, "details": ""}
if test.name == "Process List":
# Simple process list test
result["details"] = f"Current PID: {os.getpid()}"
elif test.name == "Window Enumeration":
if self.IS_WINDOWS:
try:
import ctypes
result["details"] = "ctypes available for window enum"
except ImportError:
result["success"] = False
result["details"] = "ctypes not available"
else:
result["success"] = False
result["details"] = "Not applicable on this platform"
return result
def _run_system_test(self, test: PlatformTest) -> Dict:
"""Run system-related tests."""
result = {"success": True, "details": ""}
if test.name == "CPU Info":
result["details"] = f"Processor: {platform.processor() or 'Unknown'}"
elif test.name == "Memory Info":
result["details"] = "Memory info requires psutil or platform-specific APIs"
elif test.name == "Environment Variables":
path_var = os.environ.get('PATH', '')[:100]
result["details"] = f"PATH starts with: {path_var}..."
return result
def _add_result_to_table(self, result: Dict):
"""Add a result to the results table."""
row = self.results_table.rowCount()
self.results_table.insertRow(row)
self.results_table.setItem(row, 0, QTableWidgetItem(result["name"]))
self.results_table.setItem(row, 1, QTableWidgetItem(result["category"]))
status = "✅ PASS" if result["success"] else "❌ FAIL"
status_item = QTableWidgetItem(status)
self.results_table.setItem(row, 2, status_item)
self.results_table.setItem(row, 3, QTableWidgetItem(result["details"]))
self.results_table.setItem(row, 4, QTableWidgetItem(result["platform"]))
def _update_results_summary(self):
"""Update results summary."""
passed = sum(1 for r in self._test_results if r["success"])
total = len(self._test_results)
self.results_summary.setText(f"Results: {passed}/{total} tests passed")
def _test_path_separators(self):
"""Test path separators."""
results = []
results.append(f"os.sep = '{os.sep}'")
results.append(f"os.altsep = {repr(os.altsep)}")
results.append(f"os.pathsep = '{os.pathsep}'")
results.append(f"os.linesep = {repr(os.linesep)}")
results.append("")
results.append("Pathlib tests:")
results.append(f"Path('/foo/bar').parts = {Path('/foo/bar').parts}")
self.path_results.setText("\n".join(results))
def _test_path_resolution(self):
"""Test path resolution."""
results = []
results.append(f"Path.home() = {Path.home()}")
results.append(f"Path.cwd() = {Path.cwd()}")
results.append(f"Path('~').expanduser() = {Path('~').expanduser()}")
# Test absolute/relative
test_path = Path("relative/path")
results.append(f"Path('relative/path').is_absolute() = {test_path.is_absolute()}")
results.append(f"Path('relative/path').resolve() = {test_path.resolve()}")
self.path_results.setText("\n".join(results))
def _test_unicode_paths(self):
"""Test Unicode path handling."""
results = []
test_names = [
"test_文件",
"test_🎮",
"test_ñáéíóú",
"test_مرحبا",
]
for name in test_names:
try:
test_path = Path(tempfile.gettempdir()) / name
test_path.mkdir(exist_ok=True)
exists = test_path.exists()
test_path.rmdir()
results.append(f"{name}: OK")
except Exception as e:
results.append(f"{name}: {str(e)}")
self.path_results.setText("\n".join(results))
def _test_long_paths(self):
"""Test long path handling."""
results = []
results.append("Long path support:")
results.append(f"Windows version: {platform.version()}")
results.append("")
results.append("Note: Windows 10 1607+ supports long paths")
results.append("when registry key is set and manifest declares support.")
self.path_results.setText("\n".join(results))
def _test_fcntl_locking(self):
"""Test fcntl file locking."""
results = []
try:
import fcntl
results.append("✅ fcntl module available")
# Create test file
test_file = tempfile.NamedTemporaryFile(delete=False)
test_path = test_file.name
test_file.close()
try:
with open(test_path, 'w') as f:
# Try to acquire lock
fcntl.flock(f.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB)
results.append("✅ Exclusive lock acquired")
fcntl.flock(f.fileno(), fcntl.LOCK_UN)
results.append("✅ Lock released")
finally:
os.unlink(test_path)
except ImportError:
results.append("❌ fcntl not available (expected on Windows)")
except Exception as e:
results.append(f"❌ Error: {str(e)}")
self.locking_results.setText("\n".join(results))
def _test_portalocker_locking(self):
"""Test portalocker file locking."""
results = []
try:
import portalocker
results.append("✅ portalocker available")
results.append(f"Version: {getattr(portalocker, '__version__', 'unknown')}")
except ImportError:
results.append("❌ portalocker not installed")
results.append("Install with: pip install portalocker")
self.locking_results.setText("\n".join(results))
def _test_threading_lock(self):
"""Test threading lock as fallback."""
results = []
lock = threading.RLock()
results.append("Testing threading.RLock:")
results.append(f"✅ Lock created: {lock}")
with lock:
results.append("✅ Lock acquired in main thread")
results.append("✅ Lock released")
results.append("")
results.append("Threading locks work on all platforms")
results.append("but only within a single process.")
self.locking_results.setText("\n".join(results))
def _test_file_permissions(self):
"""Test file permissions."""
results = []
test_file = Path(tempfile.mktemp())
test_file.write_text("test")
try:
stat = test_file.stat()
results.append(f"File: {test_file}")
results.append(f"Mode: {oct(stat.st_mode)}")
results.append(f"UID: {stat.st_uid}")
results.append(f"GID: {stat.st_gid}")
results.append(f"Size: {stat.st_size} bytes")
if self.IS_WINDOWS:
results.append("")
results.append("Note: Windows uses ACLs, not Unix permissions")
finally:
test_file.unlink()
self.perm_results.setText("\n".join(results))
def _test_cpu_info(self):
"""Test CPU information retrieval."""
results = []
results.append(f"Platform: {platform.platform()}")
results.append(f"Processor: {platform.processor() or 'Unknown'}")
results.append(f"Machine: {platform.machine()}")
results.append(f"CPU Count: {os.cpu_count()}")
if self.IS_LINUX:
try:
with open('/proc/cpuinfo', 'r') as f:
for line in f:
if 'model name' in line:
results.append(f"CPU: {line.split(':')[1].strip()}")
break
except:
pass
self.system_info.setText("\n".join(results))
def _test_memory_info(self):
"""Test memory information retrieval."""
results = []
try:
import psutil
mem = psutil.virtual_memory()
results.append(f"Total: {mem.total / (1024**3):.2f} GB")
results.append(f"Available: {mem.available / (1024**3):.2f} GB")
results.append(f"Used: {mem.used / (1024**3):.2f} GB")
results.append(f"Percent: {mem.percent}%")
except ImportError:
results.append("psutil not installed")
results.append("Install with: pip install psutil")
if self.IS_LINUX:
try:
with open('/proc/meminfo', 'r') as f:
results.append("")
results.append("From /proc/meminfo:")
for _ in range(3):
results.append(f.read(100))
except:
pass
self.system_info.setText("\n".join(results))
def _test_environment(self):
"""Test environment variables."""
results = []
important_vars = [
'PATH', 'HOME', 'USERPROFILE', 'APPDATA', 'TEMP', 'TMP',
'PYTHONPATH', 'PYTHONHOME', 'QT_QPA_PLATFORM',
]
results.append("Environment Variables:")
results.append("")
for var in important_vars:
value = os.environ.get(var)
if value:
# Truncate long values
if len(value) > 80:
value = value[:77] + "..."
results.append(f"{var}={value}")
else:
results.append(f"{var}=(not set)")
self.system_info.setText("\n".join(results))
def _test_window_enumeration(self):
"""Test window enumeration (Windows)."""
results = []
if not self.IS_WINDOWS:
results.append("❌ Window enumeration only available on Windows")
self.system_info.setText("\n".join(results))
return
try:
import ctypes
from ctypes import wintypes
results.append("✅ ctypes available")
# Try to enumerate windows
user32 = ctypes.windll.user32
def callback(hwnd, extra):
if user32.IsWindowVisible(hwnd):
text = ctypes.create_unicode_buffer(256)
user32.GetWindowTextW(hwnd, text, 256)
if text.value:
extra.append(text.value)
return True
EnumWindowsProc = ctypes.WINFUNCTYPE(
wintypes.BOOL,
wintypes.HWND,
wintypes.LPARAM
)
windows = []
proc = EnumWindowsProc(callback)
user32.EnumWindows(proc, 0)
results.append(f"✅ Found {len(windows)} visible windows")
results.append("")
results.append("Sample windows:")
for title in windows[:10]:
results.append(f" - {title[:50]}")
except Exception as e:
results.append(f"❌ Error: {str(e)}")
self.system_info.setText("\n".join(results))
def _export_results(self):
"""Export test results."""
if not self._test_results:
self.notify_warning("No Results", "No test results to export")
return
filename = f"platform_compat_{sys.platform}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
export_data = {
"platform": {
"system": platform.system(),
"release": platform.release(),
"version": platform.version(),
"machine": platform.machine(),
"processor": platform.processor(),
"python": platform.python_version(),
},
"timestamp": datetime.now().isoformat(),
"results": self._test_results
}
with open(filename, 'w') as f:
json.dump(export_data, f, indent=2)
self.notify_success("Exported", f"Results saved to {filename}")
plugin_class = PlatformCompatibilityTester